File Watching and Live Reload with watchfiles

· 6 min read · Updated March 21, 2026 · beginner
python file-watching watchfiles developer-tools automation

File watching lets you monitor a directory tree and respond when files are created, modified, or deleted. This is the foundation of live reload servers, automatic test runners, and build systems. The watchfiles library provides a clean Python API backed by Rust, giving you speed and simplicity.

Installation

Install watchfiles from PyPI:

pip install watchfiles

The library requires Python 3.9 or later and works on Windows, macOS, and Linux.

The Core API

The watchfiles module gives you four main functions:

FunctionDescription
watch()Synchronous generator that yields sets of file changes
awatch()Async generator version
run_process()Runs a function and restarts it when files change
arun_process()Async version of run_process()

Each change is a tuple of (Change, path_string) where Change is an IntEnum with values added, modified, and deleted.

Basic File Watching

Start with the simplest use case: watching a directory and printing changes as they happen.

import watchfiles

print("Watching current directory for changes...")
print("Press Ctrl+C to stop.")

for changes in watchfiles.watch("."):
    for change in changes:
        print(change)
Output:
Watching current directory for changes...
Press Ctrl+C to stop.
(Change.modified, '/path/to/file.py')
(Change.added, '/path/to/new_file.txt')

The watch() function accepts one or more paths as arguments. It recursively monitors all subdirectories by default.

import watchfiles

# Watch multiple directories
for changes in watchfiles.watch("/path/to/project", "/path/to/assets"):
    print(f"Detected {len(changes)} change(s):")
    for change_type, path in changes:
        print(f"  {change_type.name}: {path}")

By default, file events are debounced (collected over 1600ms and delivered as a batch). This prevents flooding you with events when many files change at once, such as during a build process.

Filtering Files

Not every change matters. Use the watch_filter parameter to ignore files you don’t care about.

import watchfiles

def py_only(change, path):
    """Only watch Python files."""
    return path.endswith(".py")

print("Watching for Python file changes...")
for changes in watchfiles.watch(".", watch_filter=py_only):
    for change in changes:
        print(change)

The filter receives a Change enum value and a string path. Return True to include the change, False to skip it.

You can also combine filters:

import watchfiles

def ignore_cache(change, path):
    """Ignore __pycache__ and .pyc files."""
    return "__pycache__" not in path and not path.endswith(".pyc")

for changes in watchfiles.watch(".", watch_filter=ignore_cache):
    print(f"Changes: {changes}")

Running a Process with Auto-Reload

The run_process() function runs any callable and restarts it when files change. Development servers benefit enormously from this.

import watchfiles

def start_server():
    print("Starting server...")
    # Your server startup code here
    print("Server ready!")

# Watch src/ and run start_server() on any file change
watchfiles.run_process("src/", target=start_server)

Every time a file in src/ changes, watchfiles terminates the previous process and calls start_server() again. This uses the spawn context under the hood, so your target function must handle re-initialisation on each call.

Control the debounce timing with debounce and step:

watchfiles.run_process(
    "src/",
    target=start_server,
    debounce=500,   # milliseconds between restart triggers
    step=50,        # milliseconds between filesystem checks
)

Lower debounce values give faster reloads but risk interrupting long-running operations. The grace_period parameter adds a delay before the first restart, giving your process time to fully start.

Building a Hot-Reload Development Server

While run_process() handles simple cases, real web servers often need more control. Here’s how to integrate watchfiles with Flask and FastAPI.

Flask Hot Reload

import os
import watchfiles
from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return {"status": "ok"}

def run_flask():
    """Reload Flask when source files change."""
    # In production, you'd use a proper WSGI server
    app.run(host="127.0.0.1", port=5000, debug=False)

if __name__ == "__main__":
    print("Starting Flask with auto-reload...")
    watchfiles.run_process(".", target=run_flask, debounce=500)

In a真实 Flask project, you’d typically separate your app creation from the run call so that module-level state resets cleanly on each reload.

FastAPI Hot Reload

import os
import watchfiles
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def index():
    return {"status": "ok"}

def run_uvicorn():
    """Reload Uvicorn when source files change."""
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000, reload=False)

if __name__ == "__main__":
    print("Starting FastAPI with auto-reload...")
    watchfiles.run_process("app/", target=run_uvicorn, debounce=500)

The key difference from uvicorn --reload is that watchfiles gives you programmatic control over the restart logic. You can combine it with custom filters to only reload on specific file changes, or add pre-restart hooks.

Async Support

If you’re working with async code, awatch() and arun_process() mirror their synchronous counterparts.

import asyncio
import watchfiles

async def watch_for_changes():
    print("Watching for changes...")
    async for changes in watchfiles.awatch("."):
        print(f"Changes detected: {changes}")

asyncio.run(watch_for_changes())

Note that awatch() cannot suppress KeyboardInterrupt. If you need clean shutdown on Ctrl+C, catch the exception at the asyncio.run() caller level.

import asyncio
import watchfiles

async def watch_for_changes():
    async for changes in watchfiles.awatch("."):
        print(f"Changes: {changes}")

try:
    asyncio.run(watch_for_changes())
except KeyboardInterrupt:
    print("\nStopped.")

Platform-Specific Considerations

On Linux, watchfiles uses inotify for efficient event-based monitoring. Some environments don’t support inotify fully:

  • WSL (Windows Subsystem for Linux): The library detects WSL and falls back to polling automatically, since inotify doesn’t work reliably over /mnt/* paths.
  • Docker containers: If your container runs with limited permissions or a different filesystem view, use force_polling=True for reliable monitoring.
  • Network filesystems: NFS, SMB, and similar mounts often require polling. Set force_polling=True in these cases.
import watchfiles

# Force polling mode for Docker or network mounts
for changes in watchfiles.watch("/app", force_polling=True):
    print(f"Changes: {changes}")

If you encounter permission errors when watchfiles tries to read subdirectories, pass ignore_permission_denied=True:

import watchfiles

for changes in watchfiles.watch(".", ignore_permission_denied=True):
    print(f"Changes: {changes}")

Stopping a Watch

Both watch() and awatch() accept a stop_event parameter. Set this event to gracefully stop watching.

import threading
import watchfiles

stop_event = threading.Event()

def worker():
    for changes in watchfiles.watch(".", stop_event=stop_event):
        print(f"Changes: {changes}")

thread = threading.Thread(target=worker)
thread.start()

# Later, when you want to stop:
stop_event.set()
thread.join()

For synchronous watch(), you can also use raise_interrupt=True to make Ctrl+C raise KeyboardInterrupt instead of silently stopping the generator.

Common Patterns

Building a simple file syncer

import os
import watchfiles
import shutil

def sync_to_backup(change, path):
    if change == watchfiles.Change.added:
        shutil.copy(path, f"backup/{path}")
    elif change == watchfiles.Change.modified:
        shutil.copy(path, f"backup/{path}")
    elif change == watchfiles.Change.deleted:
        backup_path = f"backup/{path}"
        if os.path.exists(backup_path):
            os.remove(backup_path)

Watching specific file types

import watchfiles

# Only respond to .json and .yaml config changes
config_filter = lambda c, p: p.endswith(('.json', '.yaml', '.yml'))

for changes in watchfiles.watch("config/", watch_filter=config_filter):
    print("Config changed, reloading...")

Summary

The watchfiles library gives you four functions for monitoring filesystem changes:

  • Use watch() and awatch() to detect changes and react in Python code
  • Use run_process() and arun_process() to restart processes automatically
  • Control which files trigger events with watch_filter
  • Adjust timing with debounce, step, and grace_period
  • Handle permission issues with ignore_permission_denied
  • Enable polling mode explicitly with force_polling for containers and network mounts

With these tools, you can build live reload servers, automatic test runners, file synchronisers, and any other tool that needs to respond to filesystem changes.

See Also