# 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.

```python
# 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.

## 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.

**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 is that process restart 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](https://pydevtools.com/handbook/reference/ipython.md)'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.

Neither tool 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.

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.

## 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:

```bash
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.

Despite both libraries being available and functional, neither has become a standard convention. The absence of a universal `python --watch` flag means each project handles the problem independently.

## Choosing an Approach

For web applications, use the framework-specific reload commands (Django's `runserver`, Uvicorn's `--reload`, Flask's `--reload`). For standalone scripts and CLI tools, `watchfiles` provides a lightweight restart loop. For interactive development in Jupyter or [IPython](https://pydevtools.com/handbook/reference/ipython.md), `%autoreload` handles the common case of editing imported modules during a REPL session without losing kernel state.

## Learn More

- [watchfiles documentation](https://watchfiles.helpmanual.io/) covers the standalone CLI and Python API
- [watchdog documentation](https://python-watchdog.readthedocs.io/) covers custom filesystem event handlers
- [jurigged on GitHub](https://github.com/breuleux/jurigged) documents the live-patching approach
- [IPython autoreload docs](https://ipython.readthedocs.io/en/stable/config/extensions/autoreload.html) cover `%autoreload` configuration
- [Set up a Django project with uv](https://pydevtools.com/handbook/tutorial/set-up-a-django-project-with-uv.md) walks through a project that uses Django's built-in reloader
