watchfiles review
watchfiles 1.2.0 turns operating-system file notifications into batches of `(Change, path)` tuples for Python. `watch()` is a blocking iterator, `awatch()` is an async generator, and the process helpers restart a child function or command after accepted changes. The Rust notify crate handles platform integration, while explicit polling covers mounts that do not deliver native events. This project replaces watchgod. Version 1.2 drops Python 3.9, raises the Rust source-build floor to 1.83, adds riscv64 manylinux wheels and Python 3.15 development builds, and includes type-safety and error-handling fixes.
watchfiles 1.2.0 installed in 0.2 seconds, occupied 3 MB across 4 packages, and imported in 0.29 seconds with 0 audit findings in our sandbox. It is a good default for batched Python watching and dev reloads, but choose watchdog for richer events and test polling on every non-local filesystem.
We installed it
| Install | ✓ · 0.2s | 4 packages on disk · 3 MB |
| Import | ✓ | import watchfiles in 0.29s · compiled extensions · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does watchfiles install cleanly?
Yes. In a fresh container with an empty cache, pip install watchfiles finished in 0.2s, leaving 4 packages and 3 MB on disk. pip-audit reported no known vulnerabilities.
What does watchfiles need to run?
Python >=3.10, and a platform wheel with compiled extensions. In our run import watchfiles succeeded in 0.29s, and the package ships py.typed for type checkers.
watchfiles or watchdog: which should you use?
watchdog: Choose it when observer objects, handler classes, paired move events, and directory-specific detail are required. watchfiles 1.2.0 installed in 0.2 seconds, occupied 3 MB across 4 packages, and imported in 0.29 seconds with 0 audit findings in our sandbox.
When should you not use watchfiles?
You need paired move events, separate directory event types, or handler objects. watchfiles reduces activity to added, modified, and deleted paths.
Discussed on
Use it if
- A Python tool needs recursive, batched filesystem changes without building observer and handler classes.
- Async code should consume changes with async for and stop through an asyncio or anyio event.
- Development code or a shell command must restart after filtered changes to source or configuration files.
- The same watcher must use native notifications on normal disks and switch to polling on WSL or a difficult mount.
- You need paired move events, separate directory event types, or handler objects. watchfiles reduces activity to added, modified, and deleted paths.
- The target has no published wheel and cannot build with Rust >=1.83, the source-build minimum in version 1.2.
- The application must preserve interpreter state across reloads. run_process replaces a spawned child, including its memory and open connections.
- A network or container mount needs frequent polling but its CPU or detection latency is unacceptable for the deployment.
- Your framework already runs its own reloader. Wrapping another supervisor around it can duplicate children, restarts, and signal handling.
Setup reality
Our watchfiles 1.2.0 install finished in 0.2 seconds in a fresh Python 3.12 Bookworm container. It left 4 packages using 3 MB, and import watchfiles completed in 0.29 seconds. pip-audit found 0 known vulnerabilities. The distribution declares 1 direct dependency, requires Python >=3.10, includes py.typed, ships compiled .so code, and uses the MIT license.
Published wheels cover common Linux, macOS, and Windows targets. Without a matching wheel, pip builds the Rust extension, and 1.2 requires Rust 1.83 or newer. The same release adds riscv64 manylinux wheels and Python 3.15 development coverage. No credentials or config file are required, but the process needs permission to traverse watched paths and containers must mount those paths where the watcher can see them.
Native events depend on the underlying filesystem. WSL selects polling automatically; network shares and some container volumes may need force_polling=True or WATCHFILES_FORCE_POLLING. The default collection window is 1,600 milliseconds with a 50-millisecond quiet-step check. A single editor save can yield several changes, so process the returned set as one batch and make downstream work idempotent.
run_process uses multiprocessing spawn. Function targets must be importable at module scope, and startup belongs behind if __name__ == '__main__'. Every restart discards child memory and may cut cleanup short, so align graceful shutdown with the application's signal handling. DefaultFilter ignores common VCS and temporary paths; explicitly exclude generated output or a rebuild can trigger another filesystem event and loop forever.
Patterns
Print batched changes watch-directory
from watchfiles import watch
for changes in watch('src'):
for change, path in sorted(changes, key=lambda item: item[1]):
print(change.name, path)watch blocks and yields a set after debouncing. One yield can contain several paths from one editor save.
Branch on added, modified, and deleted handle-change-kind
from watchfiles import Change, watch
for changes in watch('uploads'):
for change, path in changes:
if change is Change.deleted:
remove_index(path)
else:
update_index(path)A rename may arrive as a deletion plus an addition. watchfiles does not pair them into one move object.
Watch directories and one file together watch-several-paths
for changes in watch('src', 'templates', 'settings.toml'):
rebuild(changes)Directory traversal is recursive by default. Pass recursive=False when only direct children should count.
Consume changes from asyncio watch-async
import asyncio
from watchfiles import awatch
async def monitor():
async for changes in awatch('src'):
await queue.put(changes)
asyncio.run(monitor())Move slow work behind a bounded queue so the async generator can keep accepting filesystem batches.
Stop an async watcher with an event stop-async-watch
async def monitor(stop: asyncio.Event):
async for changes in awatch('src', stop_event=stop):
await publish(changes)awatch accepts asyncio or anyio events. The synchronous watcher expects a thread-compatible event.
Accept Python source files filter-python
from watchfiles import PythonFilter, watch
filter_ = PythonFilter(extra_extensions=('.pyi',))
for changes in watch('src', watch_filter=filter_):
run_checks(changes)PythonFilter retains standard ignored paths and adds extension filtering. extra_extensions widens the accepted suffixes.
Exclude a generated directory ignore-generated
from pathlib import Path
from watchfiles import DefaultFilter, watch
filter_ = DefaultFilter(ignore_paths=(Path('src/generated'),))
for changes in watch('src', watch_filter=filter_):
rebuild(changes)Ignoring outputs prevents the child build from causing its own next restart.
Restart an importable Python target restart-function
from watchfiles import run_process
def serve(host):
start_server(host)
if __name__ == '__main__':
run_process('src', target=serve, args=('127.0.0.1',))Spawn requires a module-level target and a main guard. Lambdas and closures are poor restart targets.
Restart a shell command restart-command
if __name__ == '__main__':
run_process('src', target='python -m pytest -q', target_type='command')Each accepted batch replaces the child process. target_type avoids relying on automatic target detection.
Run tests when Python files change use-cli
watchfiles --filter python 'python -m pytest -q' src testsQuote the child command as one argument. Do not wrap a framework that already owns a reload loop.
Poll a mount without native events force-polling
WATCHFILES_FORCE_POLLING=1 watchfiles 'python -m app' srcPolling trades idle efficiency for detection. Measure CPU use and latency on the actual mount.
Set explicit debounce timing tune-batching
for changes in watch('src', debounce=800, step=100):
rebuild(changes)Values are milliseconds. debounce caps batch collection; step is the quiet interval checked before yielding.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| watchdog | PyPI | Choose it when observer objects, handler classes, paired move events, and directory-specific detail are required. |
| watchgod | PyPI | Keep it only for a pinned legacy application while planning migration to its watchfiles replacement. |
| pyinotify | PyPI | Choose it only for Linux-specific inotify access when cross-platform behavior is unnecessary. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

