watchdog: filesystem event monitoring for Python, and when its kqueue backend stops scaling
Python library and shell utilities to monitor filesystem events.
At a glance
- What is it?
- The gorakhargosh/watchdog library wraps inotify, FSEvents, kqueue and ReadDirectoryChangesW behind one Observer API, with the watchmedo shell utility layered on top. It is a good fit for Python processes that need to react to file changes, and a poor fit for CIFS mounts or very large kqueue watches.
- Who is it for?
- Adopt watchdog if you are writing a Python process that must react to file creation, modification or deletion on Linux, macOS, Windows or BSD, and you want one Observer API across all of them. Do not adopt it if your watched tree lives on a CIFS mount without switching to PollingObserver, or if you are on kqueue and cannot raise the file descriptor limit above the number of files you watch.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 9 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What watchdog solves, and the Python processes it is built for
Most programs that care about files do not care about files at one instant. They care about files changing. A build tool wants to recompile when a source file is touched. A log shipper wants to notice a new file appearing in a spool directory. A test runner wants to rerun when the code under test is saved. Writing that yourself means picking a platform API, handling its event structure, and then writing a second implementation for the next operating system you support.
watchdog exists to collapse those implementations behind one interface. The README describes it as a Python API and shell utilities to monitor file system events, and states it works on Python 3.9 and above. The audience is Python developers who need an event-driven reaction to the filesystem rather than a loop that stats a directory every second. The library ships both an importable API and the optional watchmedo command, so the same machinery is available to a script and to a shell one-liner.
How the Observer, the event handler and the platform backends fit together
The architecture is a three-part split. An Observer is the thing that watches a directory. A FileSystemEventHandler is the thing that receives events. The Observer schedules the handler against a path, and the handler's methods are called as events arrive.
The README's example makes the split concrete. MyEventHandler subclasses FileSystemEventHandler and overrides on_any_event to print whatever event object it receives. An Observer is constructed, then schedule is called with the handler, a path of ".", and recursive=True. Then start, and the main thread sleeps in a loop until the finally block stops and joins the observer. The README also shows a context manager form, where the observer is used in a with block so the stop and join calls are handled for you.
Underneath that interface, the backend differs by platform. The README lists Linux 2.6 through inotify, macOS through FSEvents and kqueue, FreeBSD and BSD through kqueue, Windows through ReadDirectoryChangesW with I/O completion ports and worker threads, and an OS-independent fallback that polls the disk for directory snapshots and compares them periodically. The repository's setup.py shows the macOS path is not pure Python: it builds an extension named _watchdog_fsevents from src/watchdog_fsevents.c, linking CoreFoundation and CoreServices, and it skips that build on Apple device targets such as iphone or ipad unless the FORCE_MACOS_MACHINE environment variable is set to 1. So on macOS you are compiling C at install time, not just unpacking wheels.
Installing watchdog and watching a directory for the first time
The README gives two install routes. From PyPI, the base library installs with pip, and the watchmedo utility is an extra. Note that the extra is quoted in the README, which matters on shells that treat brackets specially.
python -m pip install -U watchdog
# or to install the watchmedo utility:
python -m pip install -U 'watchdog[watchmedo]'From a source checkout, the README uses an editable install with the same extra syntax:
python -m pip install -e .
# or to install the watchmedo utility:
python -m pip install -e '.[watchmedo]'After that, the smallest useful program is the README's own example. It watches the current directory recursively and prints every event object.
import time
from watchdog.events import FileSystemEvent, FileSystemEventHandler
from watchdog.observers import Observer
class MyEventHandler(FileSystemEventHandler):
def on_any_event(self, event: FileSystemEvent) -> None:
print(event)
event_handler = MyEventHandler()
observer = Observer()
observer.schedule(event_handler, ".", recursive=True)
observer.start()
try:
while True:
time.sleep(1)
finally:
observer.stop()
observer.join()Run that and touch a file in the same directory. You should see the event printed. The loop is what keeps the process alive; the observer runs its own thread, so without the sleep the script would exit immediately.
If you would rather not write Python, the README shows the watchmedo log subcommand filtering for Python and text files while ignoring directory events:
watchmedo log \
--patterns='**/*.py;**/*.txt' \
--ignore-directories \
--recursive \
--verbose \
.The companion shell-command subcommand runs a shell command per matching event, and the README's example echoes the event's source path through the watch_src_path variable:
watchmedo shell-command \
--patterns='**/*.py;**/*.txt' \
--recursive \
--command='echo "${watch_src_path}"' \
.For anything beyond those flags, the README points at watchmedo [command] --help rather than documenting the full option set inline.
watchmedo tricks: configuration-driven handlers in tricks.yaml
watchmedo has a second mode that is easy to miss. It reads tricks.yaml files and executes tricks in response to filesystem events. A trick is an event handler subclassing watchdog.tricks.Trick, and the README says trick classes are augmented with features regular event handlers do not need. The directory containing tricks.yaml is the directory that gets monitored. Each trick class is initialized with its corresponding keys from the YAML file as arguments, and events are fed to an instance of that class as they arrive.
The README's sample file shows the shape: a tricks list, where each entry is a fully qualified class name followed by its parameters. The first entry is watchdog.tricks.LoggerTrick with a patterns list of "**/*.py" and "**/*.js". The second is a third-party trick, watchmedo_webtricks.GoogleClosureTrick, configured with keys such as hash_names, mappings_format, mappings_module, suffix, compilation_level, source_directory, destination_directory, and a files mapping that groups input globs per output name.
That files mapping is the interesting part. It lets one logical output, here index-page or about-page, be assembled from a list of patterns including vendor files and page-specific globs. The design assumes you want configuration, not code, to decide what gets rebuilt. The cost is that the trick mechanism depends on plugin authors: the README describes tricks as written by plugin authors, and the only non-built-in example it gives is a package named watchmedo_webtricks. If that package is not installed, that entry in the file will not run.
Where watchdog breaks down: kqueue, CIFS and editors that swap files
The README is unusually direct about the kqueue path on macOS and BSD. It states that kqueue uses file descriptors to monitor files, so you need the number of file descriptors programs may open raised above the number of files you are monitoring, and suggests editing ~/.profile to add ulimit -n 1024 or ulimit -n unlimited. It then calls this an inherent problem with kqueue and says the combination of descriptor usage and the bookkeeping watchdog must do makes it a painful way to monitor files. Its own conclusion is that kqueue is not a very scalable way to monitor a deeply nested directory with a large number of files. That is a real ceiling, not a tuning note. If your watch tree is large and you are on a kqueue platform, the native backend is the wrong choice.
CIFS is the second boundary. The README says that to watch changes on CIFS you must explicitly use PollingObserver instead of letting watchdog pick a native observer, showing the import line from watchdog.observers.polling import PollingObserver as Observer. The reason is implied rather than stated: network filesystems do not deliver the local kernel events the native backends rely on. Polling is the fallback the README elsewhere calls slow and not recommended, so on CIFS you are choosing the slow path deliberately.
The third case is not a watchdog bug at all. The README explains that Vim does not modify files in place. It creates backup files and swaps them in, so on-modified events for files you edit in Vim will not be triggered, and you may need to configure Vim to disable that behavior. Any tool built on watchdog that assumes in-place writes will silently miss edits made this way. The same class of problem applies to other editors that write-then-rename.
Finally, the README notes free-threaded CPython support but says a full thread safety audit has not been completed, specifically affecting the macOS FSEvents interface. That is a stated unknown, not a guarantee.
watchdog compared with inotifywait and a hand-rolled polling loop
The closest alternative on Linux is inotifywait from inotify-tools. The difference is where the abstraction sits. inotifywait is a shell command that speaks the Linux inotify interface directly, so it is a natural fit for a shell script that pipes events into other commands. It is also Linux-only by construction. watchdog instead puts a Python object model over several platforms: you subclass FileSystemEventHandler, and the same code runs on Linux, macOS, Windows and BSD, with the backend chosen for you. If your reaction logic is more than a shell command, the Python API is the difference. If your reaction logic is exactly a shell command, watchmedo shell-command covers much of the same ground while keeping the cross-platform backends.
The other alternative is writing your own polling loop: stat the tree on an interval and diff the results. That is what watchdog's own OS-independent observer does, and the README calls it slow and not recommended. The trade-off is honest: polling is portable to any filesystem including CIFS and network mounts, but it costs latency equal to your interval and CPU proportional to the size of the tree. Native backends give you near-immediate events at the cost of platform-specific behavior and the kqueue descriptor ceiling. watchdog lets you pick either, and the README's CIFS guidance is essentially telling you to pick polling when the native path cannot see the mount.
Maintenance, licence and what upgrading costs you
The repository is not archived. Its last push was on 2026-09-06, which is recent enough that the project is being touched, though the release cadence is slower than the commit cadence: the most recent releases listed are v6.0.0 on 2024-11-01, v5.0.3 on 2024-09-27 and v5.0.2 on 2024-09-03. So the version you install from PyPI may lag the master branch by a wide margin. If you depend on a fix that landed after v6.0.0, you are installing from source, and the README's editable install command is the route.
Upgrade cost is dominated by the platform backends rather than the Python surface. On macOS the package builds a C extension, so a Python version bump can mean a rebuild and, if you install from source, a working compiler toolchain. The pyproject.toml pins setuptools differently for PyPy, which signals that the build is sensitive to the interpreter implementation. The README's stated floor is Python 3.9, and the Ruff configuration targets py39, so dropping older interpreters is a real part of the project's release work.
The licence is Apache-2.0, per the repository metadata and the LICENSE file at the top level. Apache-2.0 is permissive and includes an explicit patent grant, which matters if you are shipping watchdog inside a product. The repository also carries a THIRD_PARTY_LICENSES.md file, which is where you would look for the licences of anything vendored or linked. That is a pointer, not legal advice; if you redistribute a binary that includes the compiled FSEvents extension, have someone check the notices.
Editorial conclusion
Adopt watchdog if you are writing a Python process that must react to file creation, modification or deletion on Linux, macOS, Windows or BSD, and you want one Observer API across all of them. Do not adopt it if your watched tree lives on a CIFS mount without switching to PollingObserver, or if you are on kqueue and cannot raise the file descriptor limit above the number of files you watch. Verify two things first: that your target directory is reachable by the native backend rather than falling back to polling, and that your editor of choice does not swap files in a way that suppresses on-modified events, as the README says Vim does.
Frequently asked questions
How do I use watchdog in Python?
Subclass FileSystemEventHandler, override the event methods you care about such as on_any_event, then create an Observer, call schedule with the handler and a path, and call start. The README's example watches "." recursively and sleeps in a loop until the observer is stopped and joined.
How do I install watchdog?
Install from PyPI with python -m pip install -U watchdog, or use python -m pip install -U 'watchdog[watchmedo]' to get the watchmedo shell utility as well. From a source checkout the README uses an editable install, python -m pip install -e ., with the same extra for watchmedo.
How do I use watchdog on Linux?
On Linux 2.6 and later watchdog uses inotify through the same Observer interface shown in the README, so no extra configuration is needed to get native events. The watchmedo log and shell-command subcommands work there too, and they accept --patterns, --recursive and --ignore-directories.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/gorakhargosh-watchdog)