What does daemon=True change about a threading.Thread at interpreter shutdown?
answer
- It decides who the interpreter waits for
- One kind never gets to clean up
- The flag is read-only after start()
- Unset, it is inherited from the creator
- finally and __exit__ are skipped
basics
~20 sA daemon thread does not keep the process alive. At shutdown the interpreter waits for non-daemon threads only, then finalizes and stops daemon threads abruptly — no finally blocks, no context-manager cleanup. The flag must be set before start().
solid answer
~50 sThe `daemon` flag decides whether the interpreter waits for a thread before it exits. When the main thread finishes, `threading` joins every **non-daemon** thread that is still alive; the process cannot end until they return. Daemon threads are not joined: once finalization begins they are stopped at an arbitrary point, so their `finally` blocks and `__exit__` methods never run and buffered writes can be lost. You set the flag with `Thread(..., daemon=True)` or by assigning `t.daemon = True` **before** `start()` — afterwards it raises `RuntimeError: cannot set daemon status of active thread`. Unset, it is inherited from the creating thread, and since the main thread is non-daemon, top-level threads default to non-daemon. Use `daemon=True` only for fire-and-forget loops that own nothing; anything holding a file, a socket or a transaction needs a real cooperative shutdown.
code
python · 12 linesimport threading, time
def worker():
try:
while True:
time.sleep(0.05)
finally:
print("cleanup ran")
threading.Thread(target=worker, daemon=True).start()
time.sleep(0.2)
print("main exits")go deeper
Remember the one-liner: the interpreter waits for non-daemon threads and abandons daemon ones. Know that you pass daemon=True to the constructor, and that setting it after start() is an error.
Explain the shutdown sequence — non-daemon threads joined first, then finalization stops daemon threads mid-instruction with no finally and no __exit__ — and that an unset flag is inherited from the creating thread.
Demonstrate the replacement pattern: a threading.Event the worker polls, Event.wait() instead of time.sleep(), join() with a timeout, and an is_alive() check that decides whether to log, fail the shutdown, or give up on the worker.
Own the shutdown contract for the service: which work is allowed to be dropped, how long shutdown may take before the orchestrator kills the process, and why an unbounded wait for a worker is usually a worse outage than losing the background loop it was running.
## The flag is about who waits for whom Every `threading.Thread` carries a boolean `daemon` attribute, and it changes exactly one thing: whether the interpreter waits for that thread before shutting down. When the main thread runs off the end of your program (or `sys.exit()` unwinds it), CPython does **not** stop immediately. Interpreter finalization first runs `threading`'s shutdown step, which joins every non-daemon thread that is still alive. A single non-daemon worker stuck in an infinite loop is enough to hang the process forever — the familiar "my script prints its last line and never exits". Daemon threads are skipped by that join entirely. Once the non-daemon threads are done, finalization proceeds and daemon threads simply stop running. ## "Stopped abruptly" means exactly that A daemon thread is not sent an exception it can catch and it is not politely unwound. It is frozen wherever it happens to be, and the process tears down around it. Concretely: * `finally` blocks do not run. * `with` statements do not call `__exit__`, so locks are not released and files are not closed by your code. * Buffered writes that have not reached the OS are lost, which is how a daemon logger produces a truncated last line or a half-written record. * Work in flight is simply abandoned, with no record that it was abandoned. This is why "make it a daemon thread" is the wrong answer to "how do I stop my worker at exit". It is a correct answer only when the thread owns nothing whose loss matters: a metrics ticker, a cache-refresh loop, a watchdog that re-reads a config file. ## Setting and inheriting the flag Pass `daemon=True` to the constructor, or assign `t.daemon = True` before `start()`. After the thread has been started the setter raises `RuntimeError: cannot set daemon status of active thread` — it stays raised even once the thread has finished, because the check is on "was started", not "is running". If you pass nothing, the value is inherited from the thread doing the creating: threads created from the main thread default to non-daemon, but threads created *by* a daemon thread are themselves daemons, which is a common surprise in a worker that spawns helpers. There is also a hard edge at the other end of the lifecycle: starting a new thread once finalization has already begun fails. Since Python 3.13 that failure is `PythonFinalizationError`, a subclass of `RuntimeError`, which makes the case easy to recognise in a log. Code that spawns threads from `__del__` methods, `atexit` callbacks or late-firing timers is the usual way to meet it. ## The pattern that replaces it For anything that must finish or clean up, do not lean on the daemon flag; give the worker an off switch and wait for it: 1. Create a `threading.Event` and hand it to the worker. 2. The worker checks `stop.is_set()` between units of work, and — crucially — uses `stop.wait(seconds)` in place of `time.sleep(seconds)` so it wakes the moment you ask it to. 3. At shutdown, call `stop.set()`, then `t.join(timeout)`. 4. Check `t.is_alive()` after the timed join and decide what to do about a worker that did not stop: log it loudly, fail the shutdown, or — as a last resort — make it a daemon and accept the loss. Registering that sequence with `atexit`, or wrapping it in a context manager around the service's lifetime, makes it happen on every exit path rather than only the happy one. ## Judgement in production Two failure modes bracket the decision. Make everything non-daemon and one wedged worker means your process never exits, so the orchestrator's grace period expires and it gets a hard kill — usually worse than the thing you were avoiding. Make everything a daemon and shutdown is instant but lossy, with in-flight work silently dropped and no cleanup. The workable posture is: daemon for stateless background loops; non-daemon plus an explicit `Event` and a bounded `join` for anything that touches state; and a shutdown that is *bounded* — never an unbounded `join()` on a worker whose runtime you do not control.
- A daemon thread is writing to an open file when the process exits — what can go wrong?Anything still sitting in the file object's buffer is lost, because the thread is stopped without running its `finally` block or the file's `__exit__`, so nothing flushes or closes. You get a truncated final record, or a record cut mid-line. The same applies to a network send, an uncommitted transaction, or a lock the thread was holding — none of which get released by your code on that path.
- What decides the daemon flag when you don't pass one to the constructor?It is inherited from the thread that constructs the `Thread` object. The main thread is non-daemon, so threads created at top level default to non-daemon and the interpreter will wait for them. But a thread created *inside* a daemon thread inherits `daemon=True`, so a background loop that spawns helpers silently produces a whole family of threads nobody waits for.
- Why can starting a thread during interpreter shutdown fail?Once finalization has begun, the runtime will not create new threads, and the attempt raises `PythonFinalizationError` — a `RuntimeError` subclass added in Python 3.13. It usually comes from code that spawns work too late in the lifecycle: an `atexit` callback, a `__del__` method, or a timer that fires while the process is on its way out. The fix is to move the work earlier or drop it when shutdown has started.
Non-daemon threads are guests the host waits for before locking up; daemon threads are the radio left playing — the lights go out mid-song and nobody minds.
saying these in an interview costs you the question
- Says daemon threads get to run their finally blocks
- Sets the daemon flag after calling start()
- Uses daemon=True as a substitute for shutdown logic
- Thinks a daemon thread receives a catchable exception at exit
- Believes the interpreter joins every thread at exit
- Calls an unbounded join() on a worker of unknown runtime