2025, Dec 20 17:00
Undocumented aliases in Python threading.Lock: acquire_lock, release_lock, and the safer with-context pattern
Why Python threading.Lock acquire_lock and release_lock are undocumented aliases of acquire/release, and why with is the safest pattern for thread-safe code.
Undocumented twins in Python’s Lock: what acquire_lock and release_lock really are
Working with threading.Lock in Python 3.13 can be confusing when IDEs surface methods like acquire_lock, release_lock and even locked_lock alongside the public acquire, release and locked. The natural question is whether these are different code paths or just alternate names, and which ones you should actually call in production code.
The code that triggers the question
Suppose you see or write code that leans on the “lock” suffixed names:
import threading
mutex_gate = threading.Lock()
mutex_gate.acquire_lock()
try:
# critical section guarded by the lock
payload = "work"
finally:
mutex_gate.release_lock()
This runs, but it raises eyebrows: those methods are not mentioned in the public docs. Are they special in any way compared to acquire and release?
What’s actually going on
They are just aliases. In CPython, acquire_lock maps to the same underlying function as acquire, and release_lock maps to the same implementation as release. According to the project history, this has been the case for about 15 years. The names exist but are explicitly undocumented, which means they’re not part of the public API contract and can be removed at any time. By contrast, acquire() is documented, and acquire_lock() is not — that alone is a strong signal about which one real-world code is expected to use.
Some IDEs may expose the extra names in signatures or tooltips, which can be misleading. Also note that this is not how Lock is presented in threading.py’s public surface; the “lock”-suffixed variants are internal aliases rather than user-facing API.
The right way to write it
The safest and most maintainable pattern is to rely on the context manager. It acquires exactly where you start the with block and releases as soon as the block exits, including on exceptions. There’s no timing penalty compared to manual calls; it encloses precisely the lines you want protected.
import threading
sync_guard = threading.Lock()
with sync_guard:
# protected section
result = "work"
If you choose explicit acquire/release, stick to the documented names and pair them with try/finally so the lock is always released:
import threading
spin_lock = threading.Lock()
spin_lock.acquire()
try:
# protected section
outcome = "work"
finally:
spin_lock.release()
Functionally, this and the with pattern are equivalent in scope and timing. The with form is just harder to misuse.
Why knowing this matters
Undocumented APIs are brittle. Even if they work today, they have no stability guarantees and can disappear or change without notice. Using the public acquire, release and locked methods — or better, the context manager protocol — keeps your code aligned with the official contract and avoids surprises across Python versions and environments.
Practical conclusion
If you see acquire_lock, release_lock or locked_lock in your tooling, treat them as internal aliases and avoid them. Prefer with lock: for clarity and exception safety. If you need manual control, call acquire() and release() within try/finally. This small discipline keeps concurrency code predictable and resilient while matching what the documentation supports.