What states a thread moves through, and what happens to your background threads when main walks out the door.
A thread isn't just "running" or "not". It moves through a handful of states, and knowing them makes
sense of start(), join(), and why a thread can be "alive" while doing
absolutely nothing (blocked on a lock or sleeping).
start() not yet called. Nothing runs.start() was called; the OS may schedule it, but
it isn't on a CPU this instant.time.sleep. This is where I/O-bound threads spend most of their life (GIL released).target returned or raised. The thread is done and can't
be restarted.start() again raises
RuntimeError. Threads are single-use โ build a new one (or use a pool).
Every thread is either a daemon or not. The rule is simple and it's the whole point of the flag:
import threading, time
def background():
while True:
print("daemon tick")
time.sleep(0.5)
t = threading.Thread(target=background, daemon=True) # set BEFORE start()
t.start()
time.sleep(1.2)
print("main is done")
# Process exits here. The daemon is killed mid-loop โ no more ticks, no cleanup.
finally blocks, flush buffers, close files, or commit transactions. Whatever it was in the
middle of is simply abandoned.
join().
daemon before start(). Either
Thread(target=fn, daemon=True) or t.daemon = True before starting. Setting it
after the thread has started raises RuntimeError. A thread also inherits its parent's
daemon status by default.
join() Non-Daemon Threads
A non-daemon thread keeps the process alive on its own โ so you don't have to join it to avoid
killing it. You join() for a different reason: to know when it's actually done
and to surface its outcome before you move on (or before the interpreter tears down and starts closing
things out from under it).
import threading, time
def do_work(name):
time.sleep(1)
print(f"{name} finished writing its file")
t = threading.Thread(target=do_work, args=("writer",)) # non-daemon (default)
t.start()
# ... do other main-thread work here ...
t.join() # block until the writer is truly done
print("safe to continue โ the file is fully written")
join() = "wait for this,
it matters." Daemon = "background chore; I don't care if it's cut off when we exit."
The threading module gives you a small toolbox for introspection โ invaluable for logging,
debugging hangs, and health checks.
import threading, time
def worker():
print("in:", threading.current_thread().name) # the thread running THIS code
time.sleep(0.5)
t = threading.Thread(target=worker, name="Heartbeat") # name it โ shows up in logs & tracebacks
t.start()
print("alive? ", t.is_alive()) # True while running, False once terminated
print("this thread:", threading.current_thread().name) # "MainThread"
print("active: ", threading.active_count()) # how many threads are alive right now
print("all threads:", [th.name for th in threading.enumerate()]) # list every alive thread
t.join()
print("alive now? ", t.is_alive()) # False โ terminated
is_alive() โ True from after start() until
target returns/raises.name= โ give threads meaningful names; they appear in tracebacks and
logs and turn "which thread hung?" from guesswork into a glance.threading.current_thread() โ the Thread object executing
the current code.threading.active_count() โ number of currently alive
Thread objects (includes the main thread and any daemons).threading.enumerate() โ a list of all alive threads; iterate it to
see exactly what's running.
Your program starts life with one thread already running: the main thread
(threading.main_thread(), named "MainThread"). It's non-daemon, it's the one
that runs your top-level code, and it's the only thread that can receive signals like
KeyboardInterrupt (Ctrl-C).
import threading, time
def heartbeat():
while True:
# send_ping() # cheap, idempotent, safe to be cut off at any moment
time.sleep(5)
threading.Thread(target=heartbeat, name="Heartbeat", daemon=True).start()
# main thread carries on; heartbeat dies quietly whenever the app exits
join() them โ or coordinate a
graceful shutdown (Pattern 9).
| Concept | Takeaway |
|---|---|
| Lifecycle | new โ runnable โ running โ blocked/waiting โ terminated |
| Single-use | Can't restart a terminated thread โ build a new one |
daemon=True | Program exits when only daemons remain; they're killed abruptly |
| Set daemon | Before start() โ never after |
| Never daemon for | Work that must finish (writes, flushes, commits) |
join() | Wait for a non-daemon thread and know it's truly done |
| Introspection | is_alive(), name=, current_thread(), active_count(), enumerate() |