# watchfiles is a Rust extension with a Python name, and its two manifests disagree about Rust

> watchfiles gives Python code synchronous and asynchronous file watching plus process reloading, with the file system notifications handled by a Rust extension built on the notify crate. The packaging is careful, down to a stable ABI target and an explicit source distribution include list, while two details in the repository do not line up: the documented Rust requirement and the pinned one, and the dependency the async examples never use.

**samuelcolvin/watchfiles** — Simple, modern and fast file watching and code reload for Python, written in Rust

- Repository: https://github.com/samuelcolvin/watchfiles
- Website: https://watchfiles.helpmanual.io
- Stars: 2,547 · Forks: 148
- Language: Python
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/samuelcolvin-watchfiles

## The Python package is a thin layer over a cdylib built by maturin

The build system says how the two halves fit together. The build backend is maturin, required at 0.14.16 or newer and held below 2, and the Rust side is a pyo3 extension: a cdylib whose library name is _rust_notify, produced by a crate named watchfiles_rust_notify. The crate's own dependencies are short and specific, crossbeam-channel 0.5.17 for handing events across the language boundary and notify 8.2.0 for the actual file system notifications, which is the library the README credits for the underlying notifications. On the Python side there is exactly one runtime dependency, anyio at 3.0.0 or newer, and anyio even appears as a framework classifier. That is the whole runtime surface: a compiled extension plus one package. Three fields are declared dynamic rather than written out, namely license, readme and version, so the version lives in the Rust manifest, where it reads 1.3.0 and matches the v1.3.0 tag. The classifiers fill in the rest, from a production stable development status through to an intended audience that includes system administrators, which is a hint about who the package expects to be run by.

## The extension targets abi3, so one binary covers Python 3.10 and up

The pyo3 features list includes abi3-py310, and that single choice decides how the wheels are shaped. A stable ABI extension is built against the oldest supported interpreter and then loads on every later one, so the binary does not have to be rebuilt per Python version. It matches the rest of the packaging: requires-python is >=3.10, the classifiers enumerate 3.10 through 3.15 and stop there, and the README states the same range. Above that, the README says binaries are available for most architectures on Linux, MacOS and Windows, with the rest left to a source install. So the supported matrix is narrow by design at the bottom, explicit at the top, and the platform coverage is described in words rather than pinned in the manifests, since the platform classifiers cover Linux, Windows, MacOS and MacOS X without enumerating architectures.

## The README asks for Rust stable and the manifest pins 1.85

Here is a small disagreement worth catching before a source build fails. The installation section says that installing from source requires Rust stable to be installed, with no version attached. The crate manifest sets rust-version to 1.85, which is a floor rather than a floating requirement, and the lint target in the Makefile calls cargo clippy with warnings turned into errors, so a toolchain older than the floor is not a soft failure. The build itself is a one line make target, build-dev, which runs uv run maturin develop --uv against the uv managed environment rather than a global pip. maturin appears twice in the packaging with two different floors, since the build system asks for 0.14.16 or newer while the development dependency group asks for 1.8.1 or newer, and that group also pins coverage at 7.6.10 and dirty-equals at 0.8.0. Nothing in the repository reconciles any of the three version statements, so a reader with an older toolchain installed meets the difference between working and not working at the first compile.

## The package was watchgod, and the rename left no directory behind

The README states plainly that this package was previously named watchgod and points to a migration guide hosted on the documentation site. Nothing else in the repository records the old name. The top level carries a watchfiles/ directory for the Python code and a src/ directory for the Rust extension, with no watchgod/ directory and no compatibility shim listed in the tree, so the guidance for existing users lives entirely outside the repository. The same pattern runs through the packaging: the changelog entry in the project URLs points at GitHub releases rather than a file, the documentation link points at watchfiles.helpmanual.io, which is also the declared homepage, and funding points at a sponsor page rather than at an organisation. Anyone arriving from an old stack trace or an old dependency pin has three places to check and none of them in the source. One more naming detail: the crate is watchfiles_rust_notify while the Python import is watchfiles, so the Rust and Python halves of the same project do not share a name.

## Four entry points in two pairs, and the async pair is not anyio

The public surface is four functions arranged as two synchronous and asynchronous pairs. The watching pair is watch, which yields change sets:

```py
from watchfiles import watch

for changes in watch('./path/to/dir'):
    print(changes)
```

and awatch, its awaitable counterpart:

```py
import asyncio
from watchfiles import awatch

async def main():
    async for changes in awatch('/path/to/dir'):
        print(changes)

asyncio.run(main())
```

The reloading pair is run_process, which supervises a target callable:

```py
from watchfiles import run_process

def foobar(a, b, c):
    ...

if __name__ == '__main__':
    run_process('./path/to/dir', target=foobar, args=(1, 2, 3))
```

and arun_process for the same job under asyncio. The interesting detail is that both async examples import asyncio rather than anyio, even though anyio is the single declared runtime dependency and is listed as a framework classifier. The library therefore ships with a dependency its own documented usage does not reach for. The four functions are documented on two pages rather than four, with watch and awatch sharing one page and run_process and arun_process sharing another, and each example in the README is followed by a pointer to its own anchor on those pages. The repository also carries a mkdocs.yml and a docs/ directory, so the site that holds all of this is built from the same tree, which is where the migration guide mentioned earlier lives too.

## The CLI takes a quoted command and the paths after it

The command line entry point is declared in the project scripts table as watchfiles = watchfiles.cli:cli, so the console script maps straight onto a cli function in the package. Its documented shape puts the command in quotes and the watched paths after it:

```
watchfiles "some command" src
```

with the surrounding text framing it as running and reloading code when files in src change. The help output is a separate invocation rather than a documented flag list:

```bash
watchfiles --help
```

So the README gives the CLI two lines and defers the rest, including any exit codes, debounce settings or ignore patterns, to a separate CLI page on the documentation site. That split is consistent with the rest of this repository, which is a thin README over a hosted manual, and it means the behaviour of the part most people interact with is the part least described here. The only other thing the repository says about the CLI is where it comes from, the scripts table entry that binds the console script name to a cli function inside the package, which is enough to tell that reloading happens in your own Python process rather than through a supervising shell script.

## The source distribution include list is explicit, and it leaves the docs out

The crate manifest carries an include list that defines what goes into the published source distribution, and it is specific: pyproject.toml, README.md, LICENSE, the Makefile, src/, watchfiles/, tests/ and uv.lock, with explicit exclusions for __pycache__, the mypy and pytest caches under tests, and every .so file. Cargo.lock is committed in the repository but is not on that list, so it does not travel in the source distribution, which is normal for a library and worth knowing if you rely on it. What does not travel is more noticeable: docs/ and mkdocs.yml are in the repository and absent from the list, so a consumer of the source distribution cannot build the documentation site from what they received. The same manifest also carries two different repository URLs, one pointing at the project and one with an extra watchfiles segment appended to the path, so tooling that reads that field lands one directory deeper than the one in the homepage field.

## make all chains lint, types, tests, coverage and docs, and docs deletes the built extension

The Makefile sets a default goal of all, and all is a chain of four targets: lint, mypy, testcov and docs. The order matters, because the docs target starts by deleting the compiled artifacts in the package directory before running the documentation build with sync disabled, which means the extension has to be rebuilt afterwards before anything can run, and a developer who runs that target alone ends up with a source tree that no longer imports. Linting splits along the language boundary, with a Python side running ruff check and ruff format over the package and tests, and a Rust side running cargo fmt and cargo clippy with warnings denied. Setup is a separate target that syncs a frozen environment including the lint and docs groups and installs prek hooks with the overwrite flag, and a prerequisite target checks that uv is present before anything else runs. The clean target is a long list of removals covering caches, coverage output and the build directory, and the test target runs pytest under coverage rather than calling pytest directly. Formatting is a target of its own, running the Python formatter with fixes enabled and then the Rust one, so the two languages are brought back into line by a single command. The linting configuration lives in files at the root as well, with a pre-commit config, a rustfmt configuration and a coverage configuration, which means a contributor who skips the setup target loses all three.

## Conclusion

A good fit for a project that already depends on it indirectly, since it declares one runtime dependency, targets a stable ABI from Python 3.10 upward and ships prebuilt binaries for most platforms. Before building from source, note that the repository asks for Rust stable while its own manifest pins a floor of 1.85, that maturin carries two different floors in the same file, and that the async entry points are plain asyncio despite anyio being the declared dependency. Anyone upgrading from the old watchgod name should read the migration guide on the documentation site first, since that guide is not in the repository.

## FAQ

### What is watchfiles used for in Python?

It provides file watching and code reload. Four functions are exposed: watch and awatch yield change sets, and run_process and arun_process supervise a target callable and restart it when files change.

### Which Python versions does watchfiles support?

Python 3.10 through 3.15. The package metadata sets requires-python to 3.10 or newer, the extension is built against the stable ABI for 3.10, and prebuilt binaries are published for most architectures on Linux, MacOS and Windows.

### Does watchfiles need Rust installed?

Only for a source install. The README says a source install requires Rust stable, while the crate manifest sets a floor of rust-version 1.85. Everyone else uses the prebuilt binaries.

### What is watchfiles written in?

The file system notifications are handled in Rust by the notify crate, exposed to Python through a pyo3 extension module named _rust_notify and built with maturin. The Python package declares one runtime dependency, anyio.

### What happened to watchgod?

watchgod was the previous name of this package. The README points to a migration guide hosted on the documentation site, and the repository itself contains no watchgod directory or compatibility shim.

### How do I use the watchfiles command line tool?

Put the command to run in quotes and the paths to watch after it, for example watchfiles "some command" src. The console script is declared as watchfiles.cli:cli, and watchfiles --help prints the rest, which the README does not reproduce.

## Sources

- [License: MIT](https://github.com/samuelcolvin/watchfiles/blob/main/LICENSE)
- [Project website](https://watchfiles.helpmanual.io)
- [README](https://github.com/samuelcolvin/watchfiles/blob/main/README.md)
- [Releases](https://github.com/samuelcolvin/watchfiles/releases)
- [samuelcolvin/watchfiles on GitHub](https://github.com/samuelcolvin/watchfiles)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/samuelcolvin-watchfiles
