REPL-Driven Development — Deep Dive
The philosophy behind REPL-driven development
REPL-driven development (RDD) treats the interactive prompt as the primary authoring surface rather than a secondary test bed. The developer writes a tiny fragment, evaluates it, observes the result, and then decides the next fragment. Code migrates from the REPL to a module only after it proves correct interactively.
This workflow descends from Lisp’s image-based development and Smalltalk’s live object system. Python is not image-based, but its dynamic nature — runtime introspection, mutable modules, importlib.reload() — makes a surprisingly faithful approximation possible.
Setting up a professional REPL workflow
IPython startup files
IPython executes every .py file inside ~/.ipython/profile_default/startup/ on launch. Use this to preload project-specific imports:
# ~/.ipython/profile_default/startup/00-project.py
import os, sys
project_root = os.environ.get("PROJECT_ROOT", ".")
if project_root not in sys.path:
sys.path.insert(0, project_root)
This means import mypackage works immediately inside IPython without installing the package in development mode — useful when you want to explore a library you are actively editing.
Autoreload
The killer feature for RDD in Python is IPython’s autoreload extension:
%load_ext autoreload
%autoreload 2 # reload all modules before every statement
With autoreload active, you edit a .py file in your editor, switch to the REPL, and the next call uses the new code automatically. No manual importlib.reload(), no restarting the session, no re-running setup steps.
Caveats: Autoreload does not handle all cases cleanly. It struggles with:
- Enum classes (new members are not added to existing enums)
- Module-level constants cached by other modules
- C extensions
- Classes whose instances were pickled with the old definition
In practice, autoreload works flawlessly for roughly 90 percent of everyday editing. When it breaks, restarting the REPL is the safe fallback.
Rich REPL output
Install rich and configure IPython to use it:
# In startup file
try:
from rich import pretty, traceback
pretty.install()
traceback.install(show_locals=True)
except ImportError:
pass
Tracebacks now show local variable values, and data structures are colour-coded. This alone can cut debugging time significantly.
Patterns for REPL-first design
Pattern 1: Contract-first function design
Before implementing a function, define its call signature in the REPL and write the expected inputs and outputs:
# In the REPL
args = {"user_id": 42, "action": "purchase", "amount_cents": 1999}
expected = {"status": "ok", "receipt_id": "..."}
Then implement the function interactively, calling it with args after every change, comparing the output to expected. This is essentially test-driven development without the test harness overhead.
Pattern 2: Incremental pipeline building
Data pipelines benefit enormously from RDD. Build each stage as a standalone expression:
raw = load_csv("events.csv") # inspect raw.shape, raw.dtypes
cleaned = drop_nulls(raw, thresh=0.8) # inspect cleaned.shape
features = extract_features(cleaned) # inspect features.columns
Each line is its own checkpoint. If extract_features fails, you already have cleaned in memory — no need to re-run the expensive load and clean stages.
Pattern 3: Monkey-patching for exploration
When exploring a third-party library, temporarily patch methods to add logging:
_original = SomeClient.send
def _debug_send(self, *a, **kw):
print(f"SEND {a} {kw}")
return _original(self, *a, **kw)
SomeClient.send = _debug_send
After understanding the library’s behaviour, remove the patch and write proper integration code. This is far faster than reading source code or adding breakpoints inside installed packages.
Debugging live systems
breakpoint() and custom debuggers
Python 3.7 introduced the breakpoint() built-in. By default it launches pdb, but you can redirect it:
PYTHONBREAKPOINT=IPython.core.debugger.set_trace python my_script.py
Now hitting a breakpoint drops you into a full IPython session with tab completion, history, and magic commands. For remote debugging, use remote-pdb or debugpy to attach from VS Code while the process runs on a server.
Connecting a REPL to a running process
For long-lived servers (Django, FastAPI), open a management shell (python manage.py shell_plus in Django, or a custom --repl flag) that shares the application’s database connections and configuration. This lets you inspect production-like state without writing throwaway scripts.
Netflix’s engineering team has talked publicly about using IPython notebooks connected to their JVM microservices (via Polynote). In pure Python shops the equivalent is a Jupyter kernel attached to the running process via ipykernel:
# Inside your application startup
from ipykernel import embed_kernel
embed_kernel(local_ns={"app": app, "db": db_session})
A Jupyter client can then connect and explore the live application objects.
Tradeoffs and limitations
| Advantage | Limitation |
|---|---|
| Near-instant feedback | State accumulates — stale variables cause subtle bugs |
| Natural API exploration | Autoreload does not cover all code changes |
| Low ceremony debugging | Sessions are ephemeral by default — save or lose work |
| Works with any Python library | Heavy startup costs (ML models) slow session restarts |
The biggest risk is stale state. After many edits, the REPL’s namespace may contain objects created with old class definitions. Disciplined developers periodically restart and replay key setup steps. Jupyter’s “Restart and Run All” button exists precisely for this reason.
Integrating RDD into team workflows
- Commit scratch notebooks. Keep a
notebooks/exploration/directory under version control. Future team members benefit from seeing how an idea was developed. - Use
conftest.pyfixtures in the REPL. Runpytest --fixturesto list available fixtures, then replicate their setup in your startup file. This aligns REPL exploration with test infrastructure. - Document REPL recipes. A project
CONTRIBUTING.mdthat says “start withipython -i scripts/repl_setup.py” eliminates onboarding friction.
One thing to remember: REPL-driven development is not about avoiding tests or proper code structure. It is about thinking interactively, verifying assumptions instantly, and only committing code that you have already seen work with your own eyes.
See Also
- Python Literate Programming See why mixing stories and code makes programs easier to understand than code alone.
- Ci Cd Why big apps can ship updates every day without turning your phone into a glitchy mess — CI/CD is the behind-the-scenes quality gate and delivery truck.
- Containerization Why does software that works on your computer break on everyone else's? Containers fix that — and they're why Netflix can deploy 100 updates a day without the site going down.
- Python 310 New Features Python 3.10 gave programmers a shape-sorting machine, friendlier error messages, and cleaner ways to say 'this or that' in type hints.
- Python 311 New Features Python 3.11 made everything faster, error messages smarter, and let you catch several mistakes at once instead of stopping at the first one.