asyncio.Lock and Event: Synchronize Coroutines addresses a recurring problem in Python projects: Protect shared state between tasks and notify waiters when a condition becomes ready. This guide explains the mechanism, provides an executable example, and identifies the boundaries that keep an implementation reliable.
Concept and use case
Even in one event loop, a coroutine can pause at await and let another task observe intermediate state. Lock serializes a critical section; Event broadcasts a signal.
For the related fundamentals, also read the async and await guide. Integration stays simpler when functions receive dependencies and data explicitly instead of relying on global state.
Practical example
import asyncio
lock = asyncio.Lock()
ready = asyncio.Event()
balance = 0
async def add(value: int) -> None:
global balance
async with lock:
current = balance
await asyncio.sleep(0)
balance = current + value
ready.set()
Keep protected sections small and use async with so exceptions release the lock. These primitives do not synchronize operating-system threads.
Important decisions
The correct choice depends on the public contract, expected volume, and failure behavior.
Consider concurrency, empty inputs, and partial failures. Document every limit that affects consumers and choose names that express intent.
Common mistakes
A minimal example does not replace bounds, error handling, and observability. Slow I/O while holding a lock reduces concurrency and may deadlock. Event carries no payload, and its flag remains set until clear.
Avoid catching exceptions without context or returning partial output as if it were complete. An explicit failure is usually safer than silently incorrect data.
How to validate
Validate behavior, not only the happy path. Force interleaving through controlled suspension points, add test timeouts, and verify exceptions release the lock.