DEV Community

Cover image for Stop Writing Boilerplate for File Watching in Python
Michiel
Michiel

Posted on • Originally published at Medium

Stop Writing Boilerplate for File Watching in Python

File shows up → do something. Why is this still hard?

I've built more file-processing pipelines than I can count. Media ingest systems, config reloaders, data pipelines, upload processors. They all start the same way: watch a folder, react to changes.

And every single time, I end up writing the same boilerplate:

from watchfiles import watch

for changes in watch('./inbox'):
    for change_type, path in changes:
        if path.endswith('.json'):
            if change_type == Change.added:
                process_new_file(path)
            elif change_type == Change.modified:
                reload_config(path)
            # ... and so on
Enter fullscreen mode Exit fullscreen mode

It works. But it's tedious. The logic for what you're watching gets tangled with how you're watching it.

I wanted something simpler.

GitHub logo MichielMe / flowwatch

FlowWatch is a tiny ergonomic layer on top of Watchfiles that makes it easy to build file-driven workflows using simple decorators and a pretty Rich + Typer powered CLI.

FlowWatch

Python 3.11+ License: MIT Tests Coverage

FlowWatch is a tiny ergonomic layer on top of watchfiles that makes it easy to build file-driven workflows using simple decorators and a pretty Rich + Typer powered CLI.

FlowWatch Dashboard

Instead of wiring watchfiles.watch() manually in every project, you declare:

  • what folder(s) you want to watch
  • which patterns you care about (e.g. *.mxf, *.json)
  • which function should run for a given event (created / modified / deleted)

FlowWatch takes care of:

  • subscribing to all roots in a single watcher loop
  • debouncing and recursive watching
  • dispatching events to handlers with a small thread pool
  • optional processing of existing files on startup
  • nicely formatted logs and a CLI overview of registered handlers
  • real-time web dashboard for monitoring events

Installation

FlowWatch is published as a normal Python package.

# Using uv (recommended)
uv add flowwatch

# Or with pip
pip install flowwatch
Enter fullscreen mode Exit fullscreen mode

Optional Extras

# Standalone dashboard (Starlette + uvicorn)
uv
Enter fullscreen mode Exit fullscreen mode

What If File Watching Looked Like This?

from flowwatch import on_created, on_modified, run

@on_created("./inbox", pattern="*.json")
def handle_new_json(event):
    print(f"New JSON file: {event.path}")
    process_upload(event.path)

@on_modified("./config", pattern="*.yaml")
def reload_config(event):
    print(f"Config changed: {event.path}")
    app.reload_settings()

@on_created("./media", pattern="*.mxf", process_existing=True)
def ingest_media(event):
    print(f"New media file: {event.path}")
    start_transcode(event.path)

run()
Enter fullscreen mode Exit fullscreen mode

That's FlowWatch — a decorator-first layer on top of watchfiles that makes file-driven workflows readable and maintainable.

Your intent is obvious from the code. No wiring. No boilerplate.

Why I Built This

I work in broadcast technology where file-based workflows are everywhere. Media files land in watch folders. Metadata sidecars appear alongside them. Config files change.

Every project needed the same pattern:

  1. Watch one or more directories
  2. Filter by file extension
  3. Route to different handlers based on event type
  4. Maybe process files that already exist on startup

I kept copying the same wrapper code between projects. FlowWatch is that code — extracted into a proper library.

Core Concepts

The FileEvent Object

Every handler receives a FileEvent with everything you need:

@on_created("./uploads", pattern="*.pdf")
def handle_pdf(event):
    print(event.path)        # pathlib.Path to the file
    print(event.root)        # the folder being watched
    print(event.pattern)     # the pattern that matched
    print(event.change)      # watchfiles.Change enum

    # Convenience properties
    if event.is_created:
        process_new_file(event.path)
Enter fullscreen mode Exit fullscreen mode

Four Decorators, Four Intents

@on_created(root, pattern="*.txt")    # New files
@on_modified(root, pattern="*.json")  # Changed files
@on_deleted(root, pattern="*.tmp")    # Removed files
@on_any(root, pattern="*.*")          # All events
Enter fullscreen mode Exit fullscreen mode

Process Existing Files on Startup

Set process_existing=True and FlowWatch will scan the directory on startup:

@on_created("./queue", pattern="*.job", process_existing=True)
def process_job(event):
    # Runs for files already in ./queue when you start
    # Then continues watching for new ones
    execute_job(event.path)
Enter fullscreen mode Exit fullscreen mode

No more "catch up on what we missed" logic.

A Real-Time Dashboard

FlowWatch ships with an optional web dashboard:

FlowWatch Dashboard

Features:

  • Live event counters — created, modified, deleted at a glance
  • Watched directories — see exactly what folders are being monitored
  • Recent activity feed — every file event with status, path, and timestamp

Standalone Dashboard

pip install flowwatch[dashboard]
Enter fullscreen mode Exit fullscreen mode
from flowwatch import on_created, run_with_dashboard

@on_created("./inbox", pattern="*.json")
def handle_json(event):
    process_file(event.path)

run_with_dashboard(host="0.0.0.0", port=8000)
Enter fullscreen mode Exit fullscreen mode

FastAPI Integration

pip install flowwatch[fastapi]
Enter fullscreen mode Exit fullscreen mode
from fastapi import FastAPI
from flowwatch import on_created
from flowwatch.fastapi import create_flowwatch_router

app = FastAPI()

@on_created("./uploads", pattern="*.*")
def handle_upload(event):
    process_upload(event.path)

app.include_router(create_flowwatch_router(), prefix="/flowwatch")
Enter fullscreen mode Exit fullscreen mode

The CLI

FlowWatch ships with a Typer + Rich CLI:

flowwatch run myproject.watchers
Enter fullscreen mode Exit fullscreen mode

This imports your module, discovers all handlers, and shows you a formatted table:

┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┓
┃ Handler          ┃ Root             ┃ Events  ┃ Pattern  ┃
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━┩
│ handle_new_json  │ ./inbox          │ created │ *.json   │
│ reload_config    │ ./config         │ modified│ *.yaml   │
│ ingest_media     │ ./media          │ created │ *.mxf    │
└──────────────────┴──────────────────┴─────────┴──────────┘
Enter fullscreen mode Exit fullscreen mode

Options:

flowwatch run myproject.watchers \
  --debounce 500 \
  --max-workers 8 \
  --no-recursive \
  --log-level DEBUG
Enter fullscreen mode Exit fullscreen mode

Real-World Example: Media Ingest

Here's a pattern I use in production:

from pathlib import Path
from flowwatch import FileEvent, on_created, on_deleted, run

INBOX = Path("/media/incoming")

@on_created(str(INBOX), pattern="*.mxf", process_existing=True)
def ingest_media(event: FileEvent) -> None:
    """New media file arrived — start processing."""
    print(f"[ingest] New media: {event.path.name}")

    # Check for sidecar metadata
    sidecar = event.path.with_suffix(".json")
    if sidecar.exists():
        start_full_ingest(event.path, sidecar)
    else:
        queue_pending(event.path)

@on_created(str(INBOX), pattern="*.json")
def handle_sidecar(event: FileEvent) -> None:
    """Metadata sidecar arrived — check if media is waiting."""
    media_file = event.path.with_suffix(".mxf")
    if is_pending(media_file):
        start_full_ingest(media_file, event.path)

if __name__ == "__main__":
    run()
Enter fullscreen mode Exit fullscreen mode

Docker-Ready

FlowWatch runs great as a worker container:

services:
  backend:
    build: ./backend
    volumes:
      - media:/media

  flowwatch:
    build: ./backend
    command: flowwatch run myproject.watchers
    volumes:
      - media:/media
    restart: unless-stopped

volumes:
  media:
Enter fullscreen mode Exit fullscreen mode

When to Use FlowWatch

Perfect for:

  • File ingest pipelines
  • Config file reloaders
  • Upload processing queues
  • Build systems and asset pipelines
  • Any "file arrives → do something" workflow

Use something else if:

  • You need distributed file watching across machines
  • You need complex DAG-based orchestration (use Airflow, Prefect)

FlowWatch is intentionally simple — a thin layer over watchfiles, not a workflow engine.

Installation

# Core library
pip install flowwatch

# With standalone dashboard (Starlette + uvicorn)
pip install flowwatch[dashboard]

# With FastAPI integration
pip install flowwatch[fastapi]

# All features
pip install flowwatch[all]
Enter fullscreen mode Exit fullscreen mode

Or with uv:

uv add flowwatch
uv add flowwatch --extra dashboard
uv add flowwatch --extra fastapi
uv add flowwatch --extra all
Enter fullscreen mode Exit fullscreen mode

GitHub logo MichielMe / flowwatch

FlowWatch is a tiny ergonomic layer on top of Watchfiles that makes it easy to build file-driven workflows using simple decorators and a pretty Rich + Typer powered CLI.

FlowWatch

Python 3.11+ License: MIT Tests Coverage

FlowWatch is a tiny ergonomic layer on top of watchfiles that makes it easy to build file-driven workflows using simple decorators and a pretty Rich + Typer powered CLI.

FlowWatch Dashboard

Instead of wiring watchfiles.watch() manually in every project, you declare:

  • what folder(s) you want to watch
  • which patterns you care about (e.g. *.mxf, *.json)
  • which function should run for a given event (created / modified / deleted)

FlowWatch takes care of:

  • subscribing to all roots in a single watcher loop
  • debouncing and recursive watching
  • dispatching events to handlers with a small thread pool
  • optional processing of existing files on startup
  • nicely formatted logs and a CLI overview of registered handlers
  • real-time web dashboard for monitoring events

Installation

FlowWatch is published as a normal Python package.

# Using uv (recommended)
uv add flowwatch

# Or with pip
pip install flowwatch
Enter fullscreen mode Exit fullscreen mode

Optional Extras

# Standalone dashboard (Starlette + uvicorn)
uv
Enter fullscreen mode Exit fullscreen mode

If it saves you time, give it a ⭐ on GitHub. If you have ideas, open an issue — I'm actively improving it.

What file-watching patterns do you use? Let me know in the comments!

Top comments (0)