Python threads
I’m looking into threading in Python because it’s used in some code related to the bubble sort algorithm that I’m trying to understand: Simple algorithms. It’s not clear to me if the use of threads in that code significant.
What is threading?
“The threading module provides a way to run multiple threads (smaller units of a process)
concurrently within a single process. It allows for the creation and management of threads, making
it possible to execute tasks in parallel, sharing memory space. _Threads are particularly useful
when tasks are I/O bound, such as file operations or making network requests, where much of the time
is spent waiting for external resources.”1 (emphasis mine.)
I think the issue here is that I’m not understanding what’s meant by concurrent. It doesn’t mean that separate threads are being executed at the same time:
“Unlike the multiprocessing module, which uses separate processes to bypass the global
interpreter lock (GIL), the
threading module operates within a single process, meaning that all threads share the same memory
space. However, the GIL limits the performance gains of threading when it comes to CPU-bound tasks,
as only one thread can execute Python bytecode at a time. Despite this, threads remain a useful
tool for achieving concurrency in many scenarios.”2 (emphasis mine.)
Given the above, I’m not sure that threading would make a difference to the code at Cell bubble sort.
What is a thread?
“A thread is a separate flow of execution. This means that your program will have two things happening at once. But for most Python 3 implementations the different threads to not actually execute at the same time: they merely appear to.”3
“Tasks that spend much of their time waiting for external events are generally good candidates for threading.”4
Creating threads
Using the Python threading module.
To create a thread, pass it a function and a list containing the function’s arguments:
import logging
import threading
import time
def thread_function(name):
logging.info(f"Thread {name}: starting")
time.sleep(2)
logging.info(f"Thread {name}: finishing")
def main():
format = "%(asctime)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO, datefmt="%H:%M:%S")
x = threading.Thread(target=thread_function, args=(1,)) # create the thread
logging.info("Main : before running thread")
x.start() # start the thread
logging.info("Main : wait for the thread to finish")
# x.join() # `join` is still a mystery?
logging.info("Main : all done")
if __name__ == "__main__":
main()
Output:
❯ python main.py
20:08:21: Main : before running thread
20:08:21: Thread 1: starting
20:08:21: Main : wait for the thread to finish
20:08:21: Main : all done
20:08:23: Thread 1: finishing # note the 2 second delay; the call to `sleep` occurs before it
Daemon threads
In general a daemon is a program that runs as a background process. (It’s customary to name daemon
processes with the letter “d”. E.g., syslogd, a daemon that implements system logging, and sshd,
a daemon that serves incoming ssh connections.)
Python daemon threads are a bit different (see https://docs.python.org/3/library/threading.html#thread-objects).
NOTE: this all makes sense when you consider that all Python programs create a Main Thread. (See Main thread explained). The daemon flag in a thread’s constructor controls what happens when the Main thread finishes:
daemon=False(default): the program will wait for the thread to finish before exitingdaemon=True: the program will not wait for the thread to finish
Think of daemon threads as “background service” threads that should only run while the main program is active.
- a thread can be flagged as a “daemon thread”
- the significance of the “daemon” flag is that the entire Python program exits when only daemon threads are left
- the flag can be set through the
daemon(boolean) property, or the daemon constructor argument - a daemon thread will shut down immediately when the program exits
Running the code example from above, but with the daemon=True flag in the thread constructor:
x = threading.Thread(target=thread_function, args=(1,), daemon=True)
Outputs:
❯ python main.py
20:11:56: Main : before running thread
20:11:56: Thread 1: starting
20:11:56: Main : wait for the thread to finish
20:11:56: Main : all done
The Thread {name}: finishing logging statement wasn’t run. The thread with the name 1 was a
daemon thread, so when main() reaches the end of its code, the daemon thread is killed. (I’m still
unsure of the significance of this.)
The join method
The join() method tells one thread to wait for another thread to finish. My sense was that there’s
only one thread in the code example, but calling threading.enumerate() makes it clearer what’s
going on. (See the next section.)
NOTE: the x.join() method makes the Main thread wait for thread x to complete before continuing.
It’s like a synchronization point.
Without join() the Main thread continues immediately after
starting the new thread, and both run concurrently.
With join() the Main thread pauses at the join() call until the spawned thread finishes.
Now try the code with daemon=True in the constructor, and x.join() called after the x.start()
method:
# ...
x = threading.Thread(target=thread_function, args=(1,), daemon=True)
logging.info("Main : before running thread")
x.start()
logging.info("Main : wait for the thread to finish")
x.join()
logging.info("Main : all done")
# ...
Outputs:
❯ python main.py
20:14:51: Main : before running thread
20:14:51: Thread 1: starting
20:14:51: Main : wait for the thread to finish
20:14:53: Thread 1: finishing
20:14:53: Main : all done
Enumerating threads with threading.enumerate
The threading.enumerate() method returns a list of all Thread objects currently alive. The linst
includes daemonic threads, dummy thread objects created by current_thread(), and the main
thread. It excludes terminated threads and threads that have not yet been started.
# ...
x = threading.Thread(target=thread_function, args=(1,), daemon=True)
logging.info("Main : before running thread")
x.start()
print("threading.enumerate():", threading.enumerate())
logging.info("Main : wait for the thread to finish")
x.join()
# ...
Outputs:
threading.enumerate(): [<_MainThread(MainThread, started 140645025482560)>, <Thread(Thread-1 (thread_function), started daemon 140645016532672)>]
After removing the daemon flag from the Thread-1 constructor:
threading.enumerate(): [<_MainThread(MainThread, started 140495951521600)>, <Thread(Thread-1 (thread_function), started 140495942579904)>]
The main thread explained
When you run a Python program, it automatically starts in the Main thread. Any additional
threads you create with threading.Thread() run alongside it.
References
Anderson Jim, “An Intro to Threading in Python.” https://realpython.com/intro-to-python-threading/.
Python 3.14.2 Documentation. “threading — Thread-based parallelism.” https://docs.python.org/3/library/threading.html.
Wikipedia contributors. “Thread (computing).” Wikipedia, The Free Encyclopedia. https://en.wikipedia.org/w/index.php?title=Thread_(computing)&oldid=1327671967 (accessed January 6, 2026).
-
Python 3.12.2 Documentation, “threading — Thread-based parallelism,” https://docs.python.org/3/library/threading.html. ↩︎
-
ibid. ↩︎
-
Jim Anderson, “An Intro to Threading in Python,” https://realpython.com/intro-to-python-threading/. ↩︎
-
ibid. ↩︎