Zalgorithm

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:

Think of daemon threads as “background service” threads that should only run while the main program is active.


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).


  1. Python 3.12.2 Documentation, “threading — Thread-based parallelism,” https://docs.python.org/3/library/threading.html↩︎

  2. ibid. ↩︎

  3. Jim Anderson, “An Intro to Threading in Python,” https://realpython.com/intro-to-python-threading/↩︎

  4. ibid. ↩︎

Tags: