How Does Hot Reloading Work in Python?
Every Django developer knows runserver restarts when you save a file. Every FastAPI developer knows uvicorn --reload. But ask “how do I auto-reload my Python script?” and the answer fractures. There is no --watch flag built into the python command, no universal file-watching convention that works across all Python projects.
This fragmentation is not an oversight. It reflects how Python loads and manages code at runtime, and why different development contexts require different reload strategies.
How Python Loads Code
When Python encounters an import statement, it creates a module object and inserts it into sys.modules, then executes the module’s source code to populate it. Subsequent imports of the same module skip execution and return the cached object. This happens once per process.
The standard library provides importlib.reload(), which re-executes a module’s source code in the existing module namespace, rebinding names it redefines. But reload() has important limitations: names removed from the source file remain in the module namespace unless overwritten, and any other module that already imported names from the reloaded module retains the old references.
# module_a.py
def greet():
return "hello"
# module_b.py
from module_a import greet # greet is now a reference in module_b's namespace
# After editing module_a.py to return "hi" and calling importlib.reload(module_a):
# module_b.greet still returns "hello"This means “just reload the changed module” fails as a general strategy. Even reloading the entire dependency graph remains fragile because module globals are retained and existing instances keep old class definitions.
Restarting the entire process is the only generally reliable way to get a clean state.
Node.js has a similar module cache (require.cache), but its ecosystem developed a convention around process-restart tools like nodemon early on, and frontend bundlers like Vite later added true hot module replacement (HMR) with explicit accept/dispose hooks. Python’s ecosystem took a different path: individual frameworks solved the problem for their own use cases.
The Process-Restart Approach
The most reliable reload strategy is the bluntest one: detect a file change, kill the running process, start a new one. This guarantees a clean slate with no stale references or inconsistent state.
Most implementations use a parent-child process architecture. The parent process watches the filesystem; the child process runs the actual application. When a file changes, the parent kills the child and spawns a new one. This design keeps the watcher itself immune to import errors or crashes in the reloaded code.
Python’s major web frameworks all implement this pattern:
Django uses a StatReloader that polls files for modification timestamps, or optionally a WatchmanReloader that integrates with Facebook’s Watchman for filesystem event notifications. The runserver command enables reloading by default in development. When a .py file changes, Django terminates the server process and spawns a fresh one.
Flask provides the same behavior through Werkzeug’s reloader. Running flask run --reload or passing debug=True to app.run() enables it. The implementation mirrors Django’s approach: watch files, restart process.
FastAPI / Uvicorn uses watchfiles for efficient filesystem monitoring when it is installed (included in uvicorn[standard]); otherwise Uvicorn falls back to polling .py files for modification timestamps. Running uvicorn main:app --reload watches the project directory for changes and restarts the server. The --reload-dir flag scopes which directories to monitor.
The trade-off with process restart is straightforward: it loses all in-memory state. Database connections are re-established, caches are cleared, and any objects held in memory disappear.
For web servers, this cost is usually acceptable because the server re-initializes in under a second. For long-running processes that build up state over time (like a data pipeline mid-execution), it can be disruptive.
The Live-Patching Approach
An alternative strategy patches code in the running process without restarting. Instead of killing the process, the tool detects which functions or classes changed and rewrites their bytecode in place.
jurigged takes this approach for general Python scripts. It monitors source files and patches changed functions and methods in the running process, updating existing instances to use the new code. It can also selectively re-run changed module-level statements. However, it cannot retroactively rebuild program state that was constructed from earlier versions of the code, so structural changes may still require a restart.
IPython’s %autoreload does something similar for interactive sessions. After running %load_ext autoreload and %autoreload 2, IPython reimports changed modules before executing each new cell. It patches existing functions, classes, and even instances to use updated code. This works well for data exploration workflows where a researcher edits a utility module and wants the REPL to pick up changes without restarting the kernel. It still has caveats for structural changes, removed symbols, and C extensions.
The trade-off is the inverse of process restart: live patching preserves state but can produce subtle inconsistencies. If module-level initialization code runs differently, the patched functions may operate against stale module state.
Tools like jurigged and %autoreload handle many common cases well, but neither can guarantee perfect consistency after arbitrary code changes. For iterative development and data science, this is usually acceptable; for testing production-like behavior, process restart gives more trustworthy results.
Why There Is No Universal Solution
Web frameworks bundle their own reload mechanism because they control the entry point. A runserver command owns the process lifecycle: it knows when to start, how to watch, and when to restart. This makes adding file watching a natural extension of the framework’s development mode.
Generic Python scripts have no standard entry point to hook into. Running python my_script.py hands control to the script itself, with no outer process managing restarts. There is no convention for scripts to opt into reload behavior, and no standard way for an external tool to know how to restart a script correctly (does it need arguments? environment variables? a specific working directory?).
The REPL has the opposite constraint: restarting would destroy the session. Interactive development requires live patching, not process restart, which is why IPython’s %autoreload exists as a magic command within the REPL environment rather than as a separate tool.
These different requirements make a single solution impractical. A web framework’s reloader would be wrong for a REPL. A REPL’s live patcher would be unreliable for a web server. Each domain optimizes for its own constraints.
Generic File Watching
Two libraries handle filesystem monitoring for most of the Python ecosystem: watchfiles and watchdog.
watchfiles powers Uvicorn’s reload mode and also provides a standalone CLI. To rerun a script whenever source files change:
watchfiles "python my_script.py"This watches the current directory for changes and restarts the command. You can scope it to specific directories or file patterns with additional flags.
watchdog is the more established library, with both a Python API for building custom filesystem event handlers and a watchmedo shell utility for command execution and restart workflows. Werkzeug (and therefore Flask) can use watchdog for faster file-change detection. Projects that need fine-grained control over which events trigger which actions tend to use watchdog directly.
Despite both libraries being available and functional, neither has become a standard convention for general Python development. Python developers tend to reach for framework-specific reload commands when available, or rerun manually when not. The absence of a universal python --watch flag means each project handles the problem independently, and no single external tool has achieved the default status that nodemon holds in the Node.js world.
Choosing an Approach
The right reload strategy depends on the development context. For web applications, the framework-specific reload commands (Django’s runserver, Uvicorn’s --reload, Flask’s --reload) are the right starting point; they are built in and enabled by default in development mode. For standalone scripts and CLI tools, watchfiles provides a lightweight restart loop. For interactive development in Jupyter or IPython, %autoreload handles the common case of editing imported modules during a REPL session without losing kernel state.