Of course! The term "Python async watch" can mean a few different things, but it most commonly refers to watching for changes in files or directories and then performing an action, all while using Python's asyncio library for non-blocking I/O.

This is incredibly useful for:
- Live Reloading: Automatically restarting a web server or reloading a script when you save a file.
- Build Processes: Triggering a build task (like compiling Sass or bundling JS) whenever a source file changes.
- Automation: Running a test suite whenever a Python file is modified.
Let's break down how to do this, starting with the simplest approach and moving to a more robust, production-ready solution.
The Core Idea: The Watch Loop
The fundamental pattern for any "watcher" is a loop that continuously checks for changes. Here's the pseudocode:
while True:
1. Get the current state of files/directories (e.g., their modification times).
2. Sleep for a short interval (e.g., 0.5 seconds) to avoid burning CPU.
3. Get the state again.
4. Compare the old and new states.
5. If a change is detected, run your async action.
The key is to make the "sleep" part non-blocking so it doesn't hog the event loop. This is where asyncio.sleep() is perfect.

Method 1: The "From Scratch" Approach (Educational)
This method helps you understand the mechanics. We'll create a simple watcher that prints a message when a file is modified.
How it works:
- We'll use the
osmodule to get file modification times (os.path.getmtime). - We'll use
asyncio.sleep()for our polling interval. - The action to be run on change will also be an
asyncfunction.
import os
import asyncio
import time
# The file or directory we want to watch
WATCHED_FILE = "my_test_file.txt"
async def run_action_on_change():
"""The action to perform when a change is detected."""
print(f"[{time.strftime('%H:%M:%S')}] CHANGE DETECTED! Running action...")
# Simulate an async action (e.g., making an API call, running a subprocess)
await asyncio.sleep(1)
print("Action finished.")
async def watch_file():
"""Watches a file for changes."""
# Get the initial modification time
try:
last_mtime = os.path.getmtime(WATCHED_FILE)
except FileNotFoundError:
print(f"Error: {WATCHED_FILE} not found. Please create it first.")
return
print(f"Watching '{WATCHED_FILE}' for changes. Press Ctrl+C to stop.")
while True:
try:
await asyncio.sleep(0.5) # Non-blocking sleep
current_mtime = os.path.getmtime(WATCHED_FILE)
if current_mtime != last_mtime:
last_mtime = current_mtime
await run_action_on_change() # Run our async action
except FileNotFoundError:
# File was deleted, stop watching
print(f"File {WATCHED_FILE} was deleted. Stopping watcher.")
break
if __name__ == "__main__":
# Create a dummy file to watch
with open(WATCHED_FILE, "w") as f:
f.write("initial content")
# Run the watcher
try:
asyncio.run(watch_file())
except KeyboardInterrupt:
print("\nWatcher stopped by user.")
To run this:
- Save the code as
watcher.py. - Run
python watcher.py. - Open
my_test_file.txtin a text editor, save it, and watch your terminal for the "CHANGE DETECTED!" message.
Method 2: Using the watchdog Library (The Practical Approach)
Writing a robust watcher is harder than it looks. You have to handle edge cases like:

- File creation.
- File deletion.
- Directory moves.
- Performance with many files.
The watchdog library is a fantastic, cross-platform solution that handles all these complexities using native OS APIs (like inotify on Linux) for maximum efficiency.
Installation:
pip install watchdog
watchdog provides an event-based model, which is much cleaner than polling.
How it works:
- You define a class that inherits from
watchdog.events.FileSystemEventHandler. - You override methods like
on_modified(),on_created(), etc. - These methods are called by the
watchdogobserver when an event occurs. - You start the observer in a separate thread to not block your
asyncioevent loop.
Here's how to integrate it with asyncio:
import asyncio
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
WATCHED_DIR = "." # Watch the current directory
class MyEventHandler(FileSystemEventHandler):
def __init__(self, loop):
self.loop = loop
super().__init__()
def on_modified(self, event):
# We only care about files, not directories
if not event.is_directory:
print(f"[{time.strftime('%H:%M:%S')}] File modified: {event.src_path}")
# Schedule the async coroutine to be run on the loop
asyncio.run_coroutine_threadsafe(run_action_async(), self.loop)
async def run_action_async():
"""The async action to perform."""
print("-> Running async action (e.g., restarting server)...")
await asyncio.sleep(2) # Simulate a long-running async task
print("-> Async action finished.")
async def main():
"""Sets up and runs the watchdog observer."""
# Get the current asyncio loop
loop = asyncio.get_running_loop()
# Create the event handler and pass the loop to it
event_handler = MyEventHandler(loop)
# Create the observer
observer = Observer()
observer.schedule(event_handler, WATCHED_DIR, recursive=False)
print(f"Watching directory: '{WATCHED_DIR}' for changes. Press Ctrl+C to stop.")
observer.start()
try:
# Keep the main coroutine alive
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
if __name__ == "__main__":
# Create a dummy file to watch
with open("another_file.txt", "w") as f:
f.write("initial content")
asyncio.run(main())
Key differences from the "from scratch" version:
- Event-Driven:
watchdogtells us when a change happened, so we don't need to poll. - Threading: The
Observerruns in its own thread. Theon_modifiedcallback is executed in that thread. To interact with the asyncio loop, we useasyncio.run_coroutine_threadsafe(), which safely schedules our async coroutine to run on the main loop. - More Robust: It handles all file system events correctly.
Method 3: Using a Specialized Library (watchfiles)
For a modern, fast, and purely asyncio-native solution, the watchfiles library is an excellent choice. It's built on top of watchdog but provides a much cleaner async API.
Installation:
pip install watchfiles
This is often the preferred method for new async projects because it's so simple to use.
import asyncio
import time
from watchfiles import awatch
WATCHED_DIR = "."
async def run_action_async():
"""The async action to perform."""
print(f"[{time.strftime('%H:%M:%S')}] -> Running async action...")
await asyncio.sleep(2) # Simulate a long-running async task
print("-> Async action finished.")
async def main():
"""Watches for changes using watchfiles."""
print(f"Watching directory: '{WATCHED_DIR}' for changes. Press Ctrl+C to stop.")
# awatch is an async generator that yields sets of changes
async for changes in awatch(WATCHED_DIR):
# You can inspect the changes if needed, e.g., changes[0][1] is the path
# For simplicity, we just run the action on any change
print(f"Detected {len(changes)} change(s).")
await run_action_async()
if __name__ == "__main__":
# Create a dummy file to watch
with open("async_watch_file.txt", "w") as f:
f.write("initial content")
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nWatcher stopped by user.")
Why watchfiles is great:
- Simple
asyncAPI:awatch()is anasyncgenerator, making the code incredibly clean and readable. - No Threading: It handles the interaction with the underlying
watchdogthread pool internally, so you don't have to think about it. - Performance: It's highly optimized.
Summary and Recommendation
| Method | Pros | Cons | Best For |
|---|---|---|---|
| From Scratch | Great for learning the basics. No external dependencies. | Brittle, inefficient (polling), misses many edge cases. | Educational purposes, very simple scripts. |
watchdog |
Robust, cross-platform, event-driven (efficient). | Requires manual threading/run_coroutine_threadsafe to use with asyncio. |
Complex applications where you need fine-grained control over file system events. |
watchfiles |
Simple, modern, asyncio-native, very efficient. |
One more dependency, but a very good one. | Most use cases. This is the recommended approach for new async projects. |
For any serious project, start with watchfiles. It provides the best balance of simplicity, performance, and robustness for an async Python watcher.
