Deadlock comes from circular lock dependencies; break the cycle with ordering or timeouts
A deadlock needs four conditions: mutual exclusion, hold-and-wait, no preemption, and circular wait. The most common Python case is two threads acquiring two locks in opposite orders, or a thread acquiring a non-reentrant Lock twice. The standard fixes are: establish a global lock ordering and always acquire locks in that order, use a single lock instead of two when possible, use a timeout on acquire so the cycle is broken and the code can retry or fail, and prefer higher-level primitives such as queue.Queue or an executor that manage coordination internally. For debugging, faulthandler.dump_traceback_later prints all thread stacks after a timeout, which is usually enough to see the two threads waiting on each other.
Global lock ordering: assign each lock a rank and always acquire in ascending order.
Timeout: lock.acquire(timeout=5) avoids hanging forever; pair it with a clear error or retry policy.
Single lock or a higher-level primitive removes the cycle entirely.
Detect with faulthandler.dump_traceback_later(timeout=10) or by sending SIGABRT after enabling faulthandler.
Trade-off: coarse locks reduce deadlock risk but reduce concurrency. Fine-grained locks increase throughput but multiply ordering hazards.
Common mistake: acquiring a lock inside a callback or signal handler, which can reenter and deadlock.
Common mistake: holding a lock across a blocking IO call, which turns a latency problem into a deadlock under load.
Version note: faulthandler has been in the stdlib since 3.3. Lock.acquire accepts a timeout in all Python 3 versions.
0-2 years experience
2-5 years experience
5-8 years experience
8+ years experience