Wait until all threads are finished in Python
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
In Python, waiting for threads to finish usually means calling join() on each thread you started. That gives the main thread a clean synchronization point so it does not exit early or start using results before the worker threads are done. The right pattern depends on whether you are managing raw threading.Thread objects or using a higher-level API such as ThreadPoolExecutor.
Use join() with threading.Thread
The standard low-level approach is:
- create the thread
- start the thread
- call
join()when you need to wait for completion
The important detail is that join() blocks the calling thread until the target thread finishes.
join() Does Not Start the Thread
A common mistake is thinking join() somehow schedules the thread. It does not. The thread must already be running or already finished.
Wrong mental model:
- create thread
- call
join()and expect work to begin
Correct model:
- create thread
- call
start() - later call
join()to wait
If you forget start(), the thread never runs and the program logic is wrong even though the API calls look close.
Use Timeouts When You Need Bounded Waiting
join() optionally accepts a timeout, which is useful when a worker may hang or when the caller must stay responsive.
After a timed join, is_alive() tells you whether the thread is still running.
A timed join is not cancellation. It only limits how long the caller waits.
Daemon Threads Change Shutdown Behavior
If a thread is marked as a daemon thread, Python will not wait for it during interpreter shutdown in the same way as normal threads.
Daemon threads are fine for some background activities, but if your requirement is "wait until all work is done," daemon threads are usually the wrong default. A normal thread plus explicit join() is clearer and safer.
ThreadPoolExecutor Is Often Easier
For many programs, concurrent.futures.ThreadPoolExecutor is a better abstraction than managing thread objects directly.
Calling future.result() waits for each task, and leaving the with block also waits for submitted tasks to finish.
This is usually easier to manage than manually storing and joining threads.
Watch for Exceptions in Worker Threads
If a raw threading.Thread raises an exception, it does not automatically propagate back through join(). That means "all threads finished" is not the same as "all threads succeeded."
With executors, calling future.result() re-raises task exceptions in the caller, which is often a nicer failure model.
If you use raw threads and need structured error handling, plan for that explicitly rather than assuming join() will report worker failures.
Common Pitfalls
- Forgetting to call
start()beforejoin(). - Assuming
join()reports worker exceptions automatically. - Using daemon threads when the program actually needs to wait for the work to finish.
- Calling
join()too early and accidentally serializing work that was meant to overlap. - Reimplementing thread-pool behavior manually when
ThreadPoolExecutorwould be simpler.
Summary
- For raw Python threads,
join()is the standard way to wait for completion. - Start threads first, then join them when synchronization is needed.
- Use timeout joins when waiting must be bounded.
- Prefer normal threads over daemon threads when completion matters.
- For many workloads,
ThreadPoolExecutoris a cleaner way to manage waiting and results.

