Watching File Changes with watchfiles
Introduction
The watchfiles library is a file system watcher for Python that detects changes to files and directories in real time. It wraps platform-specific file system APIs (like inotify on Linux, FSEvents on macOS, and ReadDirectoryChangesW on Windows) to provide a unified interface across operating systems.
Use cases for watchfiles include:
- Automatically restarting a development server when source files change
- Triggering tests when test files are modified
- Live reloading a web application during development
- Watching configuration files and reloading settings on change
The library requires Python 3.9 or later and supports Python versions up to 3.14. Install it with pip:
pip install watchfiles
Version 1.1.1 is the current stable release.
Getting Started with Basic File Watching
The core of watchfiles is the watch() function, which yields change events as you modify files. Each event contains a Change enum indicating what happened to a file.
from watchfiles import watch, Change
# Watch the current directory for changes
for changes in watch("."):
for change_type, path in changes:
if change_type == Change.added:
print(f"File added: {path}")
elif change_type == Change.modified:
print(f"File modified: {path}")
elif change_type == Change.deleted:
print(f"File deleted: {path}")
When you run this and create, modify, or delete a file in the current directory, the watcher prints the event:
# output
File added: /path/to/example.py
File modified: /path/to/example.py
File deleted: /path/to/example.py
The Change enum has three values:
Change.added(integer value 1) — a new file was createdChange.modified(integer value 2) — an existing file was changedChange.deleted(integer value 3) — a file was removed
The watch() function returns an iterator that blocks until changes occur. Each iteration yields a set of FileChange tuples in the format (Change, str) — the change type first, then the path string.
Async Watching with awatch()
For asynchronous applications, watchfiles provides awatch(), which works with asyncio. This is useful when you’re building async web servers or running watchers alongside other async tasks.
import asyncio
from watchfiles import awatch, Change
async def main():
async for changes in awatch("."):
for change_type, path in changes:
print(f"{change_type.name}: {path}")
asyncio.run(main())
Running this and modifying files produces output like:
# output
modified: /path/to/config.json
added: /path/to/new_file.py
deleted: /path/to/old_file.txt
The async version is identical in behavior to the sync version but integrates with the asyncio event loop. Use awatch() when you need to coordinate file watching with other async operations without blocking.
Filtering Files
By default, watchfiles watches all files in the specified directories. You can filter which files to watch using filter classes or custom callables.
DefaultFilter
DefaultFilter excludes common files that usually shouldn’t trigger watches:
from watchfiles import watch, DefaultFilter
# Use the default filter (ignores __pycache__, .git, .venv, etc.)
for changes in watch(".", watch_filter=DefaultFilter):
print(changes)
# output
{Change.modified: '/path/to/app.py'}
The default filter ignores:
__pycache__directories.gitand.svndirectories.venvandvenvdirectories.DS_Storefiles- Various temporary and backup file patterns
PythonFilter
PythonFilter specifically watches .py files, ignoring everything else:
from watchfiles import watch, PythonFilter
# Only watch Python files
for changes in watch(".", watch_filter=PythonFilter):
print(changes)
# output
{Change.modified: '/path/to/main.py'}
Custom Filters
You can pass any callable that accepts a Change enum and path string, returning a boolean:
from watchfiles import watch, Change
def my_filter(change: Change, path: str) -> bool:
# Only watch files ending in .txt or .md
return path.endswith(".txt") or path.endswith(".md")
for changes in watch("src/", watch_filter=my_filter):
print(changes)
# output
{Change.modified: '/path/to/src/readme.md'}
Filter callables receive the Change enum value and the path string (not a Path object) and should return True to watch the file or False to ignore it.
Configuration Parameters
watchfiles provides several parameters to control watching behavior:
from watchfiles import watch, DefaultFilter
for changes in watch(
".",
debounce=2000, # milliseconds to wait before yielding (default 1600)
recursive=True, # watch subdirectories (default True)
watch_filter=DefaultFilter, # file filter (default None)
grace_period=5.0, # seconds to wait after startup (default 0)
force_polling=False, # use polling instead of native APIs (default False)
):
print(changes)
# output
{Change.modified: '/path/to/app.py'}
| Parameter | Default | Description |
|---|---|---|
debounce | 1600 | Milliseconds to wait after the last change before yielding |
recursive | True | Whether to watch subdirectories |
watch_filter | None | Filter function or class to exclude files |
grace_period | 0 | Seconds to wait after process starts before watching for changes |
force_polling | False | Use polling instead of native file system events |
The debounce parameter consolidates rapid successive changes into a single event. If you save a file twice within 1600ms (the default), watchfiles reports only one change. Increase debounce if you have editors that save multiple times during auto-save.
Hot Reloading with run_process()
For automatic process restarting, watchfiles provides run_process(). It runs a command and restarts it whenever watched files change:
from watchfiles import run_process
# Run a development server and restart on file changes
run_process("python main.py")
# output
INFO: Running on http://127.0.0.1:5000
INFO: Restarting due to changes...
INFO: Running on http://127.0.0.1:5000
This blocks until you press Ctrl+C. Whenever a file changes, the process terminates and restarts automatically.
You can customize the command and what files to watch:
from watchfiles import run_process, DefaultFilter
run_process(
".",
target="python -m flask run",
watch_filter=DefaultFilter,
debounce=1000,
)
# output
INFO: Watching directory '.' for changes...
INFO: Restarting due to changes...
INFO: Running on http://127.0.0.1:5000
The paths come before the target parameter. The process stdout and stderr stream to your terminal so you can see the application’s output.
Async Hot Reloading with arun_process()
For async applications, arun_process() provides the same functionality in an async context:
import asyncio
from watchfiles import arun_process
async def main():
await arun_process("python app.py")
asyncio.run(main())
# output
Application started
Restarting due to changes...
Application started
This works identically to run_process() but integrates with asyncio. Use it when your main application is async and you need to coordinate process restarting with other async tasks.
Common Gotchas
WSL Requires force_polling
If you’re running watchfiles on Windows Subsystem for Linux (WSL), native file system events may not work correctly. Enable polling explicitly:
from watchfiles import watch
for changes in watch(".", force_polling=True):
print(changes)
# output
{Change.modified: '/path/to/file.py'}
This switches from inotify to periodic polling, which works reliably in WSL environments at the cost of slightly higher CPU usage.
KeyboardInterrupt Behavior
When you press Ctrl+C to stop watch() or awatch(), the iterator raises KeyboardInterrupt. If you’re running in a loop, handle it gracefully:
from watchfiles import watch, Change
try:
for changes in watch("."):
print(changes)
except KeyboardInterrupt:
print("Watcher stopped")
# output
{Change.modified: '/path/to/app.py'}
Watcher stopped
This is expected behavior, not an error.
Debounce Consolidates Events
The debounce parameter helps avoid triggering on every keystroke during file editing, but it also means changes within the debounce window get grouped together:
# If you save a file 5 times within 1 second, you get 1 event
for changes in watch(".", debounce=1600):
print(f"Got {len(changes)} changes")
# output
Got 5 changes
Adjust debounce based on how your editor saves files. Higher values reduce events but add latency.
State Not Preserved Between Restarts
When run_process() restarts your application, it kills the old process and starts a fresh one. Any in-memory state is lost. This is typically fine for development but means you need to handle state persistence (databases, caches) separately if needed in production.
watchfiles provides a straightforward way to react to file system changes in Python. Whether you need simple change detection with watch(), async watching with awatch(), or full hot reloading with run_process(), the library handles the platform-specific details so you can focus on your application logic.