#1170·watchdog

Potential data race on WindowsApiEmitter._reader between on_thread_stop() and queue_events()

Author: ycaibbCreated Apr 27, 2026Updated Aug 15, 2026
LabelsbugwindowsHelp welcome :)

I noticed a possible unsynchronized access to WindowsApiEmitter._reader in watchdog.observers.read_directory_changes.WindowsApiEmitter.

on_thread_stop() reads and clears _reader while holding self._lock:

def on_thread_stop(self) -> None:
    with self._lock:
        reader = self._reader
        self._reader = None
    if reader is not None:
        reader.stop()

But queue_events() reads _reader without holding the same lock:

def queue_events(self, timeout: float) -> None:
    reader = self._reader
    if reader is None:
        return
    ...
    for winapi_event in reader.get_events(timeout):
        ...

If on_thread_stop() runs concurrently with queue_events(), queue_events() may retain a reference to a DirectoryChangeReader that is being stopped concurrently. This may be intentional/tolerated because the method checks for None, but the read itself is not synchronized with the write in on_thread_stop().

Would it make sense to read _reader under self._lock, similar to on_thread_stop(), e.g. by copying it while holding the lock and then using the local reference outside the lock?

Relevant lines from the current source:

  1. src/watchdog/observers/read_directory_changes.py: WindowsApiEmitter.on_thread_stop
  2. src/watchdog/observers/read_directory_changes.py: WindowsApiEmitter.queue_events Why this may matter

The race is probably benign in many cases because queue_events() exits if _reader is None, but a concurrent stop can happen after the unsynchronized read and before/during reader.get_events(timeout). At minimum, this looks like a small synchronization inconsistency around _reader lifecycle management.