When should you use asyncio.Lock over threading.Lock?

This tests cooperative multitasking knowledge: asyncio.Lock yields to the event loop via await, while threading.Lock blocks the OS thread and freezes the loop. A red flag is claiming threading.Lock works because locks are universal.
WHAT THIS TESTS: Whether the candidate understands the architectural boundary between asyncio's cooperative single-threaded concurrency and OS-level preemptive threading. Specifically, it checks if they know that asyncio tasks share one thread and yield control explicitly at await points, which means a blocking call like acquiring a threading.Lock will stall the entire event loop rather than just the current task.
A GOOD ANSWER COVERS: Four things in order. First, a concrete scenario where an asyncio.Lock is needed, such as multiple tasks incrementing a shared in-memory counter or writing to a shared connection pool between awaitable I/O operations. Second, the mechanism: asyncio.Lock is awaited, so the event loop can switch to another task while the lock is held. Third, the critical difference with threading.Lock: threading.Lock blocks the underlying native thread via OS primitives, which prevents the event loop from scheduling any other coroutine and effectively freezes all tasks. Fourth, the safety caveat from the documentation: asyncio primitives are not thread-safe, and threading primitives are not event-loop-safe, so mixing them is an architectural mismatch.
COMMON WRONG ANSWERS: Claiming that a threading.Lock is safe because it is a lock and locks protect shared state, without acknowledging the event loop freeze. Saying that asyncio.Lock is thread-safe or could be used across OS threads. Asserting that because asyncio tasks run on the same thread, no lock is ever needed, which ignores interleaving at await boundaries. Suggesting that using a threading.Lock inside an async function is fine as long as you wrap it in asyncio.to_thread, which misses the point that the question asks about tasks on the same thread.
LIKELY FOLLOW-UPS: How would you share state between an asyncio task and a thread running in an executor? What happens if a task holding an asyncio.Lock is cancelled? How does asyncio.Semaphore differ, and when would you use it instead? Can you use an asyncio.Condition with an asyncio.Lock to coordinate multiple tasks?
ONE CONCRETE EXAMPLE: Imagine a FastAPI endpoint that caches an access token in a global dictionary. Two concurrent requests hit the cache simultaneously, find it expired, and both try to refresh it. Without an asyncio.Lock, both tasks could pass the expiry check, await an HTTP request to the identity provider, and write duplicate refresh calls. With async with asyncio.Lock(), the first task acquires the lock, awaits the HTTP refresh, updates the dictionary, and releases the lock; the second task awaits the lock, sees the fresh token, and skips the HTTP call. Using threading.Lock here would block the event loop thread during the refresh, pausing all other requests on that worker.
Source: docs.python.org
Read the original → docs.python.org
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.