How to start, join, and make daemon threads in Python

Starts one daemon and one non-daemon thread, joins the non-daemon thread, and shows how daemon threads exit when the main program ends.

Easy Python 3.9+ Aug 9, 2026 Concurrency & performance 13 views 0 copies

Python code

30 lines
Python 3.9+
import threading
import time
import logging

logging.basicConfig(level=logging.INFO, format="%(threadName)s: %(message)s")

def worker(name, delay):
    for i in range(3):
        time.sleep(delay)
        logging.info(f"{name} step {i}")

if __name__ == "__main__":
    daemon_thread = threading.Thread(
        target=worker,
        args=("daemon", 0.2),
        daemon=True,
        name="DaemonThread"
    )
    main_thread = threading.Thread(
        target=worker,
        args=("non-daemon", 0.1),
        daemon=False,
        name="MainWorker"
    )

    daemon_thread.start()
    main_thread.start()

    main_thread.join()
    logging.info("Main script finished, daemon may or may not complete")

Output

stdout
MainWorker: non-daemon step 0
DaemonThread: daemon step 0
MainWorker: non-daemon step 1
DaemonThread: daemon step 1
MainWorker: non-daemon step 2
DaemonThread: daemon step 2
Main script finished, daemon may or may not complete

How it works

Threads run concurrently, so the interleaving of log messages varies between runs. The join() call on main_thread makes the main program wait until that thread finishes before logging the final message. A daemon thread is killed as soon as the main program exits—here it finishes before join() returns because its delays are shorter, but if it were longer-running, it would be abruptly terminated. Non-daemon threads keep the program alive even after main() completes, which is why joining is needed for orderly shutdown. Logging with threadName makes each thread's output easy to distinguish.

Common mistakes

  • Forgetting to call `join()`, so the program may exit before non-daemon threads finish.
  • Assuming daemon threads complete—they are terminated when the main program ends.
  • Starting a thread more than once, which raises `RuntimeError`.

Variations

  1. Use `threading.Thread(..., daemon=True)` in a `with` context via `concurrent.futures.ThreadPoolExecutor` for simpler lifecycle management.
  2. Pass `daemon=True/False` via a class that subclasses `threading.Thread` and overrides `run()`.

Real-world use cases

  • Spawn background cleanup threads that must not block application shutdown.
  • Offload periodic tasks (like heartbeat checks) to a daemon that dies with the main process.
  • Wait for critical worker threads to finish before publishing results or closing resources.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Concurrency & performance

Related tutorials and quizzes for this topic.