What does a threading.local() object give each thread, and what happens if a thread reads an attribute it never set?
answer
- One object, many private namespaces
- Nothing is shared, so nothing is locked
- The key is the thread, not the work
- An unset slot is not None
- Storage disappears with the thread
basics
~10 sA threading.local() object holds a separate attribute namespace per thread: an attribute set in one thread is invisible in every other. Reading an attribute the current thread never set raises AttributeError, not None.
solid answer
~40 s`threading.local()` returns one object whose attributes are stored per thread. Assigning `slot.user = x` writes into storage keyed by the running thread, so another thread reading `slot.user` finds nothing and gets an `AttributeError`, exactly as a missing attribute on any object would; the defensive read is `getattr(slot, "user", None)`. The object itself is a single module-level global — only the storage behind it is split, which is why nothing has to be locked. Each thread's entries are discarded when that thread terminates, so short-lived threads need no cleanup, but a long-lived pool worker keeps its values for the life of the pool. And it isolates *threads* only: coroutines sharing one event-loop thread share one slot, so per-task state belongs in a `contextvars.ContextVar` instead.
code
python · 13 linesimport threading
slot = threading.local()
slot.user = "main-thread"
def worker():
print("worker sees:", getattr(slot, "user", "<nothing set>"))
slot.user = "worker"
t = threading.Thread(target=worker)
t.start()
t.join()
print("main still sees:", slot.user)go deeper
Be ready to state the one-line contract: one object, separate attribute storage per thread, and an unset attribute raises AttributeError rather than returning None. Knowing to guard reads with getattr and a default is enough at this level.
Explain the mechanics: the assignment writes into storage keyed by the running thread, nothing is inherited by a thread you start, and entries die with the thread. Be able to say why no lock is needed.
An interviewer expects you to name the failure modes rather than the API: state missing after a hand-off to another thread, state stale on a reused worker, and no isolation at all between coroutines on one event-loop thread.
Own the design question of whether ambient per-thread state belongs in the system at all: implicit state read by any layer is invisible in signatures and hard to test, so decide deliberately where you accept it and where identity is passed explicitly.
`threading.local` answers a narrow question: how do you give every thread its own value for a name the whole program can see? The answer is **one object with split storage**. ## One object, per-thread storage `slot = threading.local()` creates a single ordinary-looking object. It is normally created once, at module level, and imported everywhere. What is *not* single is the storage behind it: every attribute you assign is written into a dictionary owned by the thread performing the assignment. `slot.user = "alice"` in one thread and `slot.user = "bob"` in another do not overwrite each other, because the two writes land in two different dictionaries. Even `slot.__dict__` is per-thread — a thread sees only its own entries there, which is why printing it from two threads gives two different answers. This inverts the usual mental model of a module-level global. A normal global is one binding shared by everybody, and mutating it from several threads is exactly the kind of shared state that needs a lock. A `threading.local` is one *name* shared by everybody and N pieces of state behind it, so there is nothing to lock: no two threads can observe the same slot. ## Reading a slot you never wrote Because the storage is per thread, a thread that never assigned an attribute simply does not have it, and reading it raises `AttributeError` — the same error a missing attribute on any object raises. It does **not** read back as `None`, and it does not fall through to some value another thread set. Two idioms cover this: read defensively with `getattr(slot, "user", None)`, or make the very first thing each thread does be to populate the slots it will need. ## Lifetime A thread's entries live exactly as long as the thread. When the thread terminates, its dictionary is dropped and the objects it referenced become collectable, so nothing has to be cleaned up explicitly for short-lived threads. The flip side matters more in production: for a *long-lived* thread — a worker in a pool that is reused for task after task — "as long as the thread" means "for the life of the pool", and a value written during one unit of work is still sitting there for the next one. ## What it does not isolate `threading.local` isolates threads and nothing else. It has no notion of a request, a task, or a unit of work, so: * Coroutines are not isolated. Every task driven by one asyncio event loop runs on the same thread, so they all share one slot and can freely clobber each other's value at an `await` point. Per-task ambient state needs `contextvars.ContextVar`, which is scoped to a context rather than a thread. * Threads do not inherit anything from the thread that started them. A newly started thread begins with an empty slot even if its parent had a value. * Generators and callbacks are not isolated either; they run on whichever thread resumes them, and read that thread's slot. ## Where it earns its place The classic uses are per-thread resources that are cheap to keep but not safe to share: a database connection or cursor bound to one thread, a parser, a random-number generator, a reusable buffer, or a "current unit of work" marker that logging code can read without every layer passing it down. In each case the point is that the object is genuinely per thread, and the thread's identity is the right key. If a subclass of `threading.local` defines `__init__`, the interpreter re-runs it the first time each new thread touches the instance — that is the documented hook for building such a per-thread resource lazily, and it surprises people who expect a constructor to run once. ## Cost and cautions Attribute access on a `threading.local` goes through per-thread lookup, so it is a little slower than a plain attribute on a plain object — irrelevant in ordinary code, worth knowing in a tight loop. The larger cost is architectural: state read implicitly by any layer is state that does not show up in a function signature, so it is easy to forget it exists until it is missing (a hand-off to another thread) or stale (a reused worker). Both failure modes trace back to the same fact, which is the thing to say in an interview: **the key is the thread, not the work.**
- What happens to the values a thread stored on a threading.local() when that thread exits?They go away with it. The per-thread storage behind the object is dropped when the thread terminates, and the objects it referenced become collectable. That is why short-lived threads need no explicit teardown, and why a long-lived worker thread is the dangerous case: its entries survive for as long as the thread does, which on a reused pool worker means the life of the pool.
- Does threading.local() isolate asyncio tasks running on one event loop?No. Isolation is per thread, and every task on one event loop runs on that loop's thread, so all of them read and write the same slot and can overwrite each other across an await. Ambient per-task state needs `contextvars.ContextVar`, which is scoped to a context rather than to a thread.
- Do you need a lock around writes to a threading.local() attribute?No, and needing one would defeat the point. Two threads cannot reach the same slot, so there is no shared mutable state to protect. A lock is only needed for the objects you *put into* a slot if you also hand references to them to other threads — but then they are no longer really per-thread state.
It is one labelled pigeonhole on the wall that every person walks up to, except each person opens it and finds only their own mail.
saying these in an interview costs you the question
- Says threading.local() returns a fresh object per thread
- Expects an unset attribute to read back as None
- Wraps writes to a thread-local slot in a lock
- Believes threading.local isolates asyncio tasks too
- Assumes a new thread inherits its parent's slot values