skip to content

When you subclass threading.local and override __init__, how often does __init__ actually run?

level: middleimportance: nice to knowfreq 12%

answer

  1. A constructor that is not really a constructor
  2. The trigger is first use, not thread creation
  3. Counted per thread, not per object
  4. The arguments are captured once and reused
  5. It is the hook for a per-thread resource

basics

~10 s

Once per thread that uses the instance, not once per instance. threading.local keeps the constructor's arguments and re-runs the subclass init with them the first time each new thread touches the object.

solid answer

~50 s

A plain `threading.local()` runs no per-thread code — the first assignment in a thread creates that thread's entry. A subclass that defines `__init__` is different: the interpreter re-invokes it the first time each new thread accesses the instance, passing the same arguments that were given to the constructor. So one object touched by four threads runs `__init__` five times. This is deliberate — it is the documented hook for lazily building a per-thread resource such as a connection, a parser or a buffer, since empty per-thread storage is usually not what a subclass wants. The traps follow from code that assumes a constructor runs once: side effects like logging, file opening or registry appends multiply by thread count, and a mutable argument is the *same* object in every thread, so it is shared rather than isolated.

code

python · 12 lines
python
import threading

class Connection(threading.local):
    def __init__(self, dsn):
        print("__init__ on", threading.current_thread().name)
        self.dsn = dsn

conn = Connection("sensors.db")
for name in ("collector-1", "collector-2"):
    t = threading.Thread(target=lambda: conn.dsn, name=name)
    t.start()
    t.join()

go deeper

for a junior

Recall that a plain threading.local() instance needs no subclass at all, and that subclassing changes when your constructor runs. Knowing that much prevents the common surprise.

for a middle

Explain the rule exactly: once per thread on that thread's first access, with the constructor's original argument objects, and be able to say why that is the intended hook for per-thread resources.

for a senior

Show the consequences you would look for in review: multiplied side effects, handles held per worker for the life of a pool, and a shared mutable argument silently defeating the isolation the class was chosen for.

for a principal

Own the judgement of when per-thread resource caching is the right shape at all, versus an explicit pool of resources with a lifecycle you can observe, size and shut down deliberately.

A bare `threading.local()` needs no initialization: the first assignment in a thread creates that thread's entry, and that is that. The surprise starts when you *subclass* it. ## The documented behaviour If a subclass of `threading.local` defines `__init__`, the interpreter calls it again the first time each new thread accesses the instance — with the same arguments that were passed to the constructor. Construct one object, touch it from four threads, and `__init__` has run five times: once at construction on the creating thread, then once per new thread on first use. The trigger is *first use in that thread*, not the thread's creation. A thread that never touches the object never runs its `__init__`. That makes it a lazy hook: the per-thread setup happens exactly when the thread needs the object, and not before. ## Why the interpreter does this The whole point of `threading.local` is that each thread gets its own storage — but storage that starts empty is often useless. The per-thread state a subclass wants usually has to be *built*: a database connection, a parser, a buffer, a random-number generator, a scratch file. Re-running `__init__` per thread is the hook that lets a subclass populate its slots without every caller remembering to. It is a feature, not an accident, and the subclass form is how you express "each thread gets one of these, constructed on demand." ## Where it bites Three failure modes account for nearly all the bugs: * **Side effects multiply.** If `__init__` logs, bumps a counter, opens a file, registers a callback or appends to a global registry, it does so once per thread rather than once per object. On a pool of workers that quietly becomes one open handle per worker, held for the life of the pool. * **Arguments are shared, not rebuilt.** The arguments are captured once, at construction, and the very same objects are handed to every per-thread `__init__`. If one of them is a mutable list or dict, every thread's instance is initialized from — and may mutate — the one shared object. The per-thread isolation you were relying on stops at the slots, not at what you put in them. * **Expensive setup runs N times.** If constructing the resource is costly, the cost scales with the number of threads that touch the object. Usually that is what you asked for; occasionally it is a surprise on a pool sized for I/O concurrency rather than for connection count. ## Testing and reasoning about it The cheap way to see it is to print the current thread's name from `__init__` and touch the instance from two threads: three lines appear. It is also worth remembering that a construction-time exception can now surface *inside an unrelated worker thread* on first use, rather than at the point where the object was built — the traceback points at a line of application code that merely read an attribute. ## What to do instead when you do not want it If the per-thread state does not need building, do not subclass at all: a plain `threading.local()` instance plus a helper that assigns the slot is simpler and runs no code per thread. If the state is genuinely per unit of work rather than per thread, neither form is right — that belongs in `contextvars.ContextVar`, because thread-keyed storage is stale on a reused worker and shared across coroutines on one event-loop thread. ## The interview answer "Once per thread that uses it, not once per object, with the constructor's original arguments — and that is the documented way to build a per-thread resource lazily." Then name the trap: side effects and mutable arguments, both of which assume a constructor runs once.

  • Is that re-initialization a defect to work around, or the intended design?
    Intended. Per-thread storage that starts empty is rarely useful, so re-running the subclass `__init__` is the hook that lets each thread build its own resource on first use. It becomes a defect only when the constructor has side effects — logging, opening handles, appending to a registry — that were written assuming they happen once.
  • What arguments does each per-thread __init__ receive?
    The ones passed to the constructor, captured once and reused — the very same objects, not fresh ones. So a mutable argument such as a list or dict is shared by every thread's initialization, which quietly defeats the isolation people expect from a thread-local. Pass immutable values, or a factory the __init__ calls.
  • Does __init__ run when a thread starts, or at some other moment?
    At that thread's first access to the instance. A thread that never touches the object never runs it, which makes the setup lazy and demand-driven. One consequence worth knowing: an exception raised during that setup surfaces inside whichever thread first used the object, with a traceback pointing at an ordinary attribute read.

saying these in an interview costs you the question

  • Says __init__ runs exactly once, at construction time
  • Expects each thread to get freshly built arguments
  • Thinks subclassing is required for per-thread attributes
  • Assumes __init__ fires when the thread is started
  • Puts one-time side effects in the subclass __init__

context