mrkeyoor.com_
Sun 20 Sept 11:44 UTC
PyPIUtilsupdated 20 Sept 2026

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.

Verdict

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

Lab card: what happened when we installed watchfilesScreenshot of watchfiles documentation
Install✓ · 0.2s4 packages on disk · 3 MB
Importimport watchfiles in 0.29s · compiled extensions · py.typed · requires Python >=3.10
Known vulns0(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.

API stability4/5watch, awatch, run_process, arun_process, Change, and the filter classes remain the recognizable 1.x surface. Operational defaults still affect behavior: debounce, step, polling choice, ignored paths, recursion, and spawn semantics can change when environments move. Services that rely on exact restart timing should pass those values explicitly and include real filesystem tests.
Docs5/5The official site documents synchronous and asynchronous watching, stop events, filters, process supervision, polling controls, CLI syntax, debounce versus step, and migration from watchgod. Signatures list defaults and return types, and examples are short enough to run directly. Network filesystems and container signal delivery still need local verification because no reference page can model every volume driver.
Maintenance5/5Version 1.2.0 shipped on May 18, 2026, and the unarchived repository was pushed on August 9, 2026. GitHub reports 48 open issues and pull requests plus 2,527 stars. The current release adds a riscv64 wheel, tests Python 3.15 development versions, updates the minimum Rust toolchain, and improves type safety and error handling.
Ecosystem5/5The package records about 84.6 million weekly downloads. Its sync iterator, async generator, CLI, and child-process supervisor cover libraries and development tools, while anyio connects async use to several event-loop contexts. Much of the usage comes through frameworks, which is useful evidence of integration but also a reason to check whether the application already has watchfiles indirectly.

Discussed on

  1. hnWatchfiles: Simple, modern and fast file watching for Python, written in Rust33 points

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.
Skip it if

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 tests

Quote 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' src

Polling 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

PackageRegistryPick it when
watchdogPyPIChoose it when observer objects, handler classes, paired move events, and directory-specific detail are required.
watchgodPyPIKeep it only for a pinned legacy application while planning migration to its watchfiles replacement.
pyinotifyPyPIChoose 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.