What is the difference between threading.Lock and threading.RLock in Python?
answer
- Re-entering code you already guard
- Does the lock know who holds it?
- A counter, not a boolean
- Balance every acquire with a release
basics
~20 sthreading.Lock is not reentrant: a thread that already holds it blocks forever if it acquires again. threading.RLock records an owner thread and a recursion count, so the same thread may re-enter and must release once per acquire.
solid answer
~50 s`threading.Lock` is a plain binary mutex with no notion of an owner. It is either locked or unlocked, and a second `acquire()` blocks whoever calls it — including the thread that is already holding it, which is a self-deadlock. `threading.RLock` is a *recursive* mutex: it remembers which thread owns it and how many times that thread has acquired it, so nested `with rlock:` blocks and mutually recursive guarded methods work. The count must go back to zero before another thread gets in, so every `acquire()` needs its own `release()`. Only the owning thread may release an `RLock`; calling `release()` from another thread raises `RuntimeError`. Prefer `Lock` by default — it is cheaper and it makes accidental re-entrancy fail loudly rather than quietly succeed — and reach for `RLock` when a public method that takes the lock legitimately calls another that also takes it. `threading.Condition()` builds on an `RLock` when you construct it without a lock argument.
code
python · 11 linesimport threading
lock = threading.Lock()
rlock = threading.RLock()
with rlock:
with rlock:
print("same thread re-entered the RLock")
with lock:
print("second acquire of the same Lock:", lock.acquire(blocking=False))go deeper
Be ready to state the one-line difference — Lock is not reentrant, RLock is — and to say what happens concretely when a thread re-acquires a Lock it already holds: it blocks forever, silently. Know that both work with a with block.
Explain the mechanics: RLock stores an owner and a recursion count, needs one release per acquire, and raises RuntimeError if a non-owner releases it. Be able to show the recursive-method case that motivates it and the wrapper-plus-helper refactor that avoids it.
Show judgement about when re-entrancy is a design smell rather than a requirement, and how you diagnose a self-deadlock in a live process — threads parked in acquire with no exception and no log. Be ready to justify defaulting to Lock.
Own the locking convention for a codebase: which objects are guarded by which lock, whether public methods may call one another under the lock, and whether you standardise on non-reentrant locks to keep that call graph honest. Say what the free-threaded build changes about the stakes.
Both objects are mutual-exclusion primitives from the `threading` module, and both support the context-manager protocol, so the idiomatic use of either is `with the_lock:` rather than a hand-written `acquire()`/`release()` pair. The difference is entirely about **ownership and re-entry**. ### threading.Lock — a binary flag with no owner A `threading.Lock` has exactly two states. `acquire()` returns `True` when it flips the lock from unlocked to locked; if the lock is already locked, the calling thread blocks until someone releases it. There is no record of *who* locked it. Two consequences follow directly: * **It is not reentrant.** If the thread that holds the lock calls `acquire()` again, it blocks waiting for itself, and nothing will ever release it. That is a self-deadlock, and it presents in production as one thread parked forever with no exception and no log line. * **Any thread may release it.** Because the lock has no owner, a `release()` from an unrelated thread is accepted. Releasing a lock that is *not* held raises `RuntimeError`, but releasing one held by a different thread does not. `acquire()` takes two useful arguments. `acquire(blocking=False)` returns `False` immediately instead of waiting, which is the safe way to *test* a lock — note that `locked()` is only a snapshot and is racy the moment it returns. `acquire(timeout=0.5)` waits at most that long and returns `False` if it gives up. Both are how you avoid an unbounded park in code that must stay responsive. ### threading.RLock — an owner plus a counter A `threading.RLock` stores the identity of the owning thread and a recursion depth. When the owner acquires it again, the depth increments and the call returns immediately. When a *different* thread acquires it, that thread blocks exactly as it would on a `Lock`. `release()` decrements the depth; the lock only becomes available to other threads when the depth reaches zero. Only the owner may release it — a release from a non-owning thread raises `RuntimeError`. The classic reason to want one is a class whose public methods each guard themselves and sometimes call one another: ```python import threading class Ledger: def __init__(self): self._lock = threading.RLock() self._entries = [] def add(self, entry): with self._lock: self._entries.append(entry) def add_batch(self, entries): with self._lock: for entry in entries: self.add(entry) # re-enters the same lock ``` With a plain `Lock`, `add_batch` would hang on the first inner `add`. The usual alternative is to split each guarded public method into a thin locking wrapper plus an unlocked `_add_locked` helper that assumes the lock is already held — more code, but it documents the locking discipline explicitly and keeps the cheaper primitive. ### Which to reach for Default to `Lock`. It is the simpler object, it is slightly faster, and its self-deadlock on re-entry is a *feature* during development: it surfaces a call graph you did not know re-entered a critical section. Reach for `RLock` when re-entry is genuinely part of the design and restructuring the call graph would be worse. Treat a codebase that uses `RLock` everywhere as a smell — it usually means nobody knows which functions hold what. Two details interviewers like to probe. First, an `RLock` acquired twice needs two releases; a single `release()` in a `finally` after two acquires leaves the lock held forever, which is the mirror image of the self-deadlock. Second, `threading.Condition()` constructed with no arguments creates an `RLock` internally, which is why `with cond:` nests safely. Finally, none of this changes because CPython has a global interpreter lock. The GIL serializes bytecode execution; it does not make a multi-step read-modify-write atomic, because a thread switch can land between bytecodes. On the free-threaded build — officially supported as of Python 3.14 — there is not even that incidental serialization, so explicit locks are the only thing standing between you and interleaved updates.
- What happens if a thread calls release() on a threading.RLock it does not own?It raises `RuntimeError`. An `RLock` records its owner, so only the acquiring thread can decrement the recursion count. A plain `threading.Lock` behaves differently: it has no owner, so another thread's `release()` is accepted — only releasing a lock that is entirely unheld raises `RuntimeError`.
- Which lock does threading.Condition() use when you construct it with no arguments?An `RLock`. That is why `with cond:` can be nested inside another block that already holds the same condition, and why `wait()` can correctly release and later re-acquire it. You can pass your own lock — including a plain `Lock` — as the constructor argument when you want to share one lock between a condition and other guarded code.
- How do you avoid blocking forever when acquiring a threading.Lock?Pass `blocking=False` for an immediate `True`/`False` answer, or `timeout=<seconds>` to wait a bounded time and get `False` if it gives up. Neither raises. Use the return value — a `with lock:` block always waits indefinitely, so bounded acquisition has to be written as an explicit `acquire()` plus `try`/`finally`.
A Lock is a toilet-door bolt: the door does not care who slid it, and if you forget you already bolted it you stand outside forever. An RLock is a hotel keycard tied to your name that counts how many times you swiped it in.
saying these in an interview costs you the question
- Says RLock lets several threads hold the lock at once
- Thinks a second Lock.acquire raises instead of blocking
- Releases an RLock once after acquiring it twice
- Uses RLock everywhere as a safer default
- Claims another thread can release an RLock it does not own
- Believes the GIL removes the need for either lock