Writing a Python watcher that reacts to filesystem events
Modern Python developers rarely work in isolation from the operating system underneath their code. Whether you are managing log files, syncing directories, or building a custom intrusion detection hook, the ability to react the moment a file changes is a powerful capability. For a senior developer like Phil Schwartz, whose portfolio includes utilities such as DenyHosts and Scratchy, filesystem awareness has long been part of the Linux toolset. Python makes this kind of monitoring surprisingly accessible thanks to a handful of well-maintained libraries.
Australia's sysadmins and DevOps engineers have a particular set of reasons to care about event-driven scripts. A shop in Sydney watching Apache logs for suspicious crawlers, a research lab in Adelaide archiving Bureau of Meteorology datasets, or a small business in Brisbane pushing nightly compliance reports to a shared drive each face moments where waiting for a cron job is not fast enough. Responding to filesystem events in real time closes that gap and turns reactive maintenance into something closer to a reflex.
This article walks through the practical decisions involved in building a Python script that monitors filesystem events and triggers actions. You will see how to choose a library, structure the project, write a working watcher, design handlers that do something useful, and finally harden the result for production use on Linux servers common across Australian hosting providers.
Understanding filesystem events and where they fit in your stack
Filesystem events are notifications emitted by the kernel whenever something happens to a file or directory. On Linux this is handled by inotify, on BSD by kqueue, and on Windows by ReadDirectoryChangesW. Python abstracts these differences through libraries, so your code can stay portable while still taking advantage of low-level hooks that fire within milliseconds of a change.
The most useful event types to think about are creation, modification, deletion, and movement. A log analyzer like Scratchy cares about modification and rotation, while a backup utility cares about creation and deletion. Once you map your business problem onto these primitives, the script becomes a thin coordinator between the kernel and whatever action you want to run next.
Picking a Python library for real-time monitoring
Several Python libraries sit on top of inotify and friends. The most popular is watchdog, a cross-platform package with a clean observer pattern, sensible defaults, and good documentation. Alternatives include pyinotify for Linux-only projects that want closer access to the underlying API, and python-polling for environments where inotify is unavailable.
When evaluating libraries, watch for three things: how they handle recursive directory watching, how they batch or debounce events, and whether they expose the underlying event metadata. For a script running on a server in a Melbourne data centre, recursive watching is usually essential because configuration trees spread across many subdirectories. Debouncing matters when you are tracking files written by noisy applications such as database servers, which can emit dozens of events for a single logical write.
Setting up a clean project layout for your watcher
A small monitoring script deserves a small but tidy structure. Put the watcher entry point in a module, separate handlers into their own file, and keep configuration external so the script can be tuned without code changes. A typical layout looks like:
watcher.pycontaining the main loop and observer setuphandlers/directory with one module per actionconfig.yamlorconfig.tomldescribing watched paths and rulesrequirements.txtpinningwatchdogand any other dependencies
This separation pays off the moment you need to add a second handler. Want to email an admin in Perth when a sensitive file changes, but only log the event when a transient file appears? Each handler can subscribe independently without touching the watcher itself.
Writing your first event-driven script
Here is a minimal example using watchdog that prints every modification event under a watched directory:
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
class PrintHandler(FileSystemEventHandler):
def on_modified(self, event):
if not event.is_directory:
print(f"Modified: {event.src_path}")
if __name__ == "__main__":
path = "/var/log/myapp"
observer = Observer()
observer.schedule(PrintHandler(), path, recursive=True)
observer.start()
try:
while True:
pass
except KeyboardInterrupt:
observer.stop()
observer.join()
The Observer runs on its own thread, which is why the main loop does nothing. In practice you would replace pass with a sleep, a queue consumer, or a graceful shutdown signal handler. Australian deployments often use systemd unit files with Type=simple so the process can be stopped cleanly via systemctl stop.
Designing flexible triggers and actions
The real value of a watcher lies in the actions it triggers. Common patterns that benefit from filesystem watchers include:
- Rotating and compressing logs when they cross a size threshold
- Rebuilding a static site or documentation bundle when source files change
- Sending alerts to a Slack channel or PagerDuty when sensitive paths are touched
- Synchronising a local working directory with a remote backup target
Each of these maps onto a handler that decides whether the event is interesting and what to do about it. Keep the decision logic in the handler, not the watcher, so the watcher remains a dumb pipe. This also makes handlers easy to unit test by passing synthetic event objects rather than waiting for real filesystem activity.
Hardening and deploying watchers in production
A script that works on your laptop will not survive long on a production server without a few extra steps. Run it under systemd with restart policies, redirect its stdout and stderr to a dedicated log file, and make sure watched paths exist before the observer starts. For Australian organisations subject to the Privacy Act and the Notifiable Data Breaches scheme, treat the watcher itself as a sensitive component, because it can see every file path on the system it monitors.
Finally, think about time zones and clock skew. Servers in Sydney, Brisbane, and Perth all run different local times, so if your handler timestamps events, use UTC internally and format for display only at the edge. With those pieces in place, your Python watcher becomes a reliable piece of automation that earns its place in the toolbox.
