mrkeyoor.com_
Sun 20 Sept 08:57 UTC
PyPIUtilsupdated 20 Sept 2026

watchdog review

Our sandbox installed watchdog 6.0.0 in 0.2 seconds, and importing it took 0.03 seconds. It turns operating-system file notifications into Python events for creates, edits, moves, deletions, opens, and closes. Linux uses inotify, macOS uses FSEvents or kqueue, Windows uses ReadDirectoryChangesW, and PollingObserver compares directory snapshots where native events do not arrive. Handlers run from an observer thread, while the optional watchmedo command can launch or restart shell commands. Version 6.0.0 moved Linux inotify waiting to select.poll(), fixed a descriptor-close race, and removed the watchmedo log command's redundant --trace option.

Verdict

watchdog remains a sensible cross-platform choice when you need detailed event objects and can test each target filesystem. Prefer watchfiles for a new async changed-path loop, and avoid watchdog when kqueue scale or unreleased symlink fixes are hard requirements.

We installed it

Lab card: what happened when we installed watchdogScreenshot of watchdog documentation
Install✓ · 0.2s1 package on disk · 1 MB
Importimport watchdog in 0.03s · pure Python · py.typed · requires Python >=3.9
Known vulns0(pip-audit)

Answers from our run

Does watchdog install cleanly?

Yes. In a fresh container with an empty cache, pip install watchdog finished in 0.2s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.

What does watchdog need to run?

Python >=3.9, and nothing compiled: it is pure Python. In our run import watchdog succeeded in 0.03s, and the package ships py.typed for type checkers.

watchdog or watchfiles: which should you use?

watchfiles: Choose it for sync or async changed-path iteration backed by Rust when handler classes and watchdog event objects are unnecessary. watchdog remains a sensible cross-platform choice when you need detailed event objects and can test each target filesystem.

When should you not use watchdog?

Python 3.8 is still in support; watchdog 6.0.0 requires Python 3.9 or newer after the project dropped 3.8 in the previous major

API stability4/5Observer, schedule(), FileSystemEventHandler, and the on_created, on_modified, on_moved, and on_deleted callbacks have remained the main public shape across several majors. Version 5 enforced keyword-only calls and renamed lower-level classes, while the unreleased 7.0.0 plan changes pattern matching enough to require edits from *.py to **/*.py. Ordinary handlers have a stable route; code using positional arguments, pattern filters, or backend internals needs closer release-note review.
Docs4/5The Read the Docs site has a typed quick start, API pages for observers and events, utility references, and links to the changelog. The repository README is unusually direct about CIFS requiring PollingObserver, kqueue file-descriptor costs, Vim replacement saves, and the incomplete free-threaded audit for FSEvents. It offers less help for application architecture: queue backpressure, handler exception policy, backend-by-backend event differences, and shutdown ownership still require reading source or testing.
Maintenance3/5GitHub reports an unarchived repository pushed on August 20, 2026, with 242 open issues and pull requests combined. Active work has fixed move-path generation, a dispatcher join hang, BSD permission handling, and Windows restart signaling. PyPI still serves 6.0.0 from November 1, 2024, while those fixes and new symlink handling remain collected under 7.0.0-dev. The source is moving, but users cannot consume that work as a stable release yet.
Ecosystem5/5The supplied registry snapshot records 27,882,474 weekly downloads, and GitHub reports 7,396 stars. The project supports Linux inotify, macOS FSEvents and kqueue, Windows ReadDirectoryChangesW, BSD kqueue, plus a polling backend. PyPI metadata exposes a watchmedo extra, and our package check found py.typed plus a successful 0.03-second import on Python 3.12. That reach makes watchdog easy to encounter in reloaders and automation, even though backend behavior still needs platform tests.

Use it if

  • One Python service must watch local directories on Linux, macOS, and Windows through the same Observer and event-handler API
  • You need source and destination paths for moves, directory events, or Linux-only open and close notifications rather than a simple changed-path list
  • A CIFS share cannot deliver native notifications and periodic directory snapshots through PollingObserver are acceptable
  • A development command needs watchmedo auto-restart or shell-command behavior without writing an observer loop
Skip it if

Setup reality

Our fresh Python 3.12 sandbox installed watchdog 6.0.0 successfully in 0.2 seconds. It left 1 package occupying 1 MB, and the measurement recorded 1 direct dependency. pip-audit found 0 known vulnerabilities. The package requires Python 3.9 or newer, is pure Python, carries Apache-2.0 licensing, and ships py.typed for type checkers. A plain import worked in 0.03 seconds. No credentials, native compiler, or service process were needed for that check.

A plain install gives you the library. Install watchdog[watchmedo] when you need the command's YAML-based tricks, which brings PyYAML. Application code creates a handler, schedules it on an Observer, starts the observer thread, then calls stop() and join() during shutdown. Core arguments such as recursive are keyword-only. Handler failures occur on the dispatch thread, so catch and log exceptions where silent watcher death would leave the main process looking healthy.

Observer chooses a backend from the host platform. Local Linux paths use inotify, macOS selects FSEvents, Windows uses ReadDirectoryChangesW, and BSD uses kqueue. CIFS needs an explicit PollingObserver import, as the README shows. Polling repeatedly snapshots the tree and is documented as slow. On kqueue, the open-file limit must exceed the watched file count. Native event sets also differ: open and close callbacks from inotify should not be treated as portable signals.

Filesystem events describe what the backend saw, which may differ from the user's mental model of a save. Vim commonly writes a replacement file, so on_modified may never fire for the original path. Keep callbacks short and hand expensive work to a queue. PatternMatchingEventHandler in 6.0.0 uses path.match() semantics. Do not copy the unreleased 7.0.0 **/*.py migration into a 6.0.0-only config unless you have tested both forms against your directory layout.

Patterns

Print every event under a directory watch-directory-tree

import time
from watchdog.events import FileSystemEvent, FileSystemEventHandler
from watchdog.observers import Observer


class Handler(FileSystemEventHandler):
    def on_any_event(self, event: FileSystemEvent) -> None:
        print(event.event_type, event.src_path, event.is_directory)


observer = Observer()
observer.schedule(Handler(), './src', recursive=True)
observer.start()
try:
    while observer.is_alive():
        time.sleep(1)
finally:
    observer.stop()
    observer.join()

Pass recursive by keyword. stop() requests shutdown, while join() waits for the observer thread to finish.

Read both sides of a rename handle-file-moves

from watchdog.events import FileMovedEvent, FileSystemEventHandler


class MoveHandler(FileSystemEventHandler):
    def on_moved(self, event: FileMovedEvent) -> None:
        if not event.is_directory:
            print(f'{event.src_path} -> {event.dest_path}')

dest_path belongs to move events. Treat a move as its own operation instead of guessing from separate delete and create callbacks.

Watch Python files and ignore build output filter-path-patterns

from watchdog.events import PatternMatchingEventHandler

handler = PatternMatchingEventHandler(
    patterns=['*.py'],
    ignore_patterns=['*/build/*', '*/.venv/*'],
    ignore_directories=True,
    case_sensitive=True,
)
handler.on_modified = lambda event: print('changed', event.src_path)

These are 6.0.0 path.match() patterns. The unreleased 7.0.0 line plans full-match semantics, where recursive forms such as **/*.py become necessary.

Drop unwanted events before dispatch filter-event-classes

from watchdog.events import FileCreatedEvent, FileMovedEvent
from watchdog.observers import Observer

observer = Observer()
observer.schedule(
    handler,
    '/var/spool/inbox',
    recursive=False,
    event_filter=[FileCreatedEvent, FileMovedEvent],
)

event_filter limits what the emitter queues. It is useful when parent-directory modification noise is irrelevant.

Use snapshots for a CIFS share watch-cifs-by-polling

from watchdog.observers.polling import PollingObserver

observer = PollingObserver(timeout=2.0)
observer.schedule(handler, '/mnt/shared', recursive=True)
observer.start()

The README requires PollingObserver for CIFS. Each interval scans directory state, so test the cost against the actual tree size.

Schedule two roots on one observer watch-multiple-directories

from watchdog.observers import Observer

observer = Observer()
source_watch = observer.schedule(handler, './src', recursive=True)
template_watch = observer.schedule(handler, './templates', recursive=True)
observer.start()

Keep the returned watches if either root may need to be removed while the observer remains alive.

Unschedule one directory remove-active-watch

watch = observer.schedule(handler, './generated', recursive=True)
observer.start()

# Later, leave the observer running for its other paths.
observer.unschedule(watch)

unschedule() expects the ObservedWatch returned by schedule(), not the original path string.

Hand thread events to asyncio bridge-to-asyncio

import asyncio
from watchdog.events import FileSystemEvent, FileSystemEventHandler


class AsyncQueueHandler(FileSystemEventHandler):
    def __init__(self, loop: asyncio.AbstractEventLoop, queue: asyncio.Queue):
        self.loop = loop
        self.queue = queue

    def on_any_event(self, event: FileSystemEvent) -> None:
        self.loop.call_soon_threadsafe(self.queue.put_nowait, event)

Handlers run outside the asyncio thread. call_soon_threadsafe performs the supported handoff; bound or dropping queues need an explicit overload policy.

Find changes between two scans compare-directory-snapshots

from watchdog.utils.dirsnapshot import DirectorySnapshot, DirectorySnapshotDiff

before = DirectorySnapshot('/data/imports')
run_import_step()
after = DirectorySnapshot('/data/imports')

diff = DirectorySnapshotDiff(before, after)
print(diff.files_created)
print(diff.files_modified)
print(diff.files_moved)

This avoids a background observer when only the before-and-after difference matters. Moved files are returned as source and destination pairs.

Keep callback failures visible log-handler-errors

import logging
from watchdog.events import FileSystemEvent, FileSystemEventHandler

log = logging.getLogger(__name__)


class SafeHandler(FileSystemEventHandler):
    def on_any_event(self, event: FileSystemEvent) -> None:
        try:
            process_event(event)
        except Exception:
            log.exception('watchdog callback failed for %s', event.src_path)

An exception is raised on the dispatch thread rather than the main application path. Decide whether to log, retry, or signal process shutdown.

Invoke tests through watchmedo run-command-on-change

python -m pip install 'watchdog[watchmedo]'

watchmedo shell-command \
  --patterns='*.py' \
  --ignore-directories \
  --recursive \
  --command='pytest -q' \
  .

The watchmedo extra installs PyYAML for YAML tricks. shell-command reacts to events, so filters should exclude directories and generated paths that would retrigger the command.

Restart a server after Python changes restart-development-process

watchmedo auto-restart \
  --patterns='*.py' \
  --recursive \
  --debounce-interval=0.5 \
  -- python app.py

The debounce interval collapses a short event burst before restarting. Version 6.0.0 does not include the unreleased Windows process-group signaling fix listed for 7.0.0-dev.

Alternatives

PackageRegistryPick it when
watchfilesPyPIChoose it for sync or async changed-path iteration backed by Rust when handler classes and watchdog event objects are unnecessary
pyinotifyPyPIChoose it only for an older Linux-only codebase already built directly around inotify concepts
inotify-simplePyPIChoose it for a thin Linux inotify wrapper when you will implement recursion, move pairing, and dispatch yourself

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.