4. Threads and Threading
By Bernd Klein. Last modified: 07 Aug 2026.
Threads in Python
General Definition of a Thread

A thread is an execution path within a process. Every process has at least one thread and may create additional threads. Multiple threads belonging to the same process can execute concurrently.
Threads within a process share the same address space. This means that they can access the same Python objects and, for example, shared module-level variables. At the same time, each thread has its own execution state, including its own call stack and the local variables of the functions it is currently executing.

Sharing data is one of the advantages of threads, but it can also cause problems. If several threads modify the same data at the same time, race conditions may occur. Python provides synchronization mechanisms such as locks to protect such critical sections.
Threads are particularly useful for I/O-bound tasks, such as network access, file operations, or waiting for external programs. While one thread is waiting for an I/O operation to complete, another thread can continue executing. This can make a program more responsive and allows several waiting operations to overlap.
Threads, CPU Cores, and the GIL
With the standard build of CPython, it is important to distinguish concurrency from true parallelism. The Global Interpreter Lock (GIL) normally allows only one thread at a time to execute Python bytecode. CPU-bound Python calculations therefore usually do not become faster simply by adding more threads.
For CPU-bound work, multiprocessing or
concurrent.futures.ProcessPoolExecutor is often a better choice,
because separate processes can make use of multiple CPU cores in parallel.
Free-threaded builds of CPython were introduced experimentally in Python 3.13. Since Python 3.14, free-threaded Python is officially supported, although it remains optional. In a free-threaded build, the GIL can be disabled and Python threads can execute Python code in parallel on multiple CPU cores. Thread-safe programs should therefore not rely on the GIL to protect shared mutable data.
Kernel Threads and User-Space Threads
In computer science, a distinction is sometimes made between threads managed by
the operating system and threads managed mainly by an application or runtime
environment. For practical Python programming, this distinction is usually of
minor importance. CPython's threading module uses native operating
system threads.
Threads in Python
Python provides several levels of abstraction for working with threads:
_thread: a very small low-level interface,threading: the standard object-oriented threading interface,concurrent.futures.ThreadPoolExecutor: a convenient high-level interface for running many similar tasks.
For new programs, threading or ThreadPoolExecutor should
normally be preferred.
The Low-Level _thread Module
In Python 2, the low-level module was called thread. In Python 3 it
was renamed to _thread. Older Python programs may therefore contain:
from thread import start_new_thread
The corresponding Python 3 import would be:
from _thread import start_new_thread
For new programs, however, this low-level interface is rarely necessary.
The threading module builds on _thread and provides a
more convenient and better structured interface.
The threading Module
We start with a simple example. Ten threads each wait for five seconds and print a message before and after waiting:
import time
from threading import Thread
def sleeper(number):
print(f"Thread {number} sleeps for 5 seconds")
time.sleep(5)
print(f"Thread {number} woke up")
threads = []
for number in range(10):
thread = Thread(target=sleeper, args=(number,))
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
Thread(...) first creates a thread object. The target
argument specifies the function to be executed, and the arguments for that
function are supplied as a tuple through args.
The start() method starts the new thread. Internally, the thread then
invokes its run() method. If target has been supplied,
the default implementation of run() calls that target function.
There is therefore no need to override run() when using
target.
The join() method makes the calling thread wait until the selected
thread has terminated. In the example above, the main thread therefore waits for
all ten worker threads.
The order in which the final ten messages appear is not defined. A possible output is:
Thread 0 sleeps for 5 seconds Thread 1 sleeps for 5 seconds Thread 2 sleeps for 5 seconds Thread 3 sleeps for 5 seconds Thread 4 sleeps for 5 seconds Thread 5 sleeps for 5 seconds Thread 6 sleeps for 5 seconds Thread 7 sleeps for 5 seconds Thread 8 sleeps for 5 seconds Thread 9 sleeps for 5 seconds Thread 2 woke up Thread 0 woke up Thread 4 woke up Thread 1 woke up Thread 7 woke up Thread 3 woke up Thread 9 woke up Thread 5 woke up Thread 6 woke up Thread 8 woke up
Shared Data and Race Conditions
Because multiple threads can access the same objects, care must be taken when shared data is modified. Consider, for example:
counter += 1
Conceptually, this is a read-modify-write operation: the current value is read, incremented, and then stored again. Code should not assume that several threads can safely perform such updates to shared state without synchronization.
A Lock provides mutual exclusion. Only one thread at a time can
execute a critical section protected by the same lock:
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100_000):
with lock:
counter += 1
threads = [threading.Thread(target=increment) for _ in range(4)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print(counter)
The statement with lock: effectively acquires the lock when the block
is entered and releases it when the block is left. This form is preferable to
manual calls to acquire() and release(), because the lock
is also released if an exception occurs inside the protected block.
For more complex communication patterns, the threading module also
provides synchronization primitives such as Event,
Condition, Semaphore, and Barrier.
The queue.Queue class is often useful for safely exchanging data
between threads.
Defining a Thread Class
Instead of passing a function through target, we can subclass
threading.Thread and override its run() method.
The following example tests whether numbers are prime:
import threading
class PrimeNumber(threading.Thread):
def __init__(self, number):
super().__init__()
self.number = number
self.is_prime = None
def run(self):
if self.number < 2:
self.is_prime = False
return
divisor = 2
while divisor * divisor <= self.number:
if self.number % divisor == 0:
self.is_prime = False
return
divisor += 1
self.is_prime = True
numbers = [97, 99, 101, 1733]
threads = [PrimeNumber(number) for number in numbers]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
if thread.is_prime:
print(f"{thread.number} is a prime number")
else:
print(f"{thread.number} is not a prime number")
This example demonstrates how to define a custom thread class. It is not intended as a way to speed up large prime-number calculations with a standard CPython build, because primality testing is CPU-bound. Processes or another form of true parallelism are more appropriate for CPU-intensive work.
ThreadPoolExecutor
When many similar tasks have to be executed concurrently,
concurrent.futures.ThreadPoolExecutor is often easier to use than
creating and managing individual thread objects manually.
A thread pool contains a limited number of worker threads. Tasks are submitted to the pool and executed by available workers. Their results can then be collected when the work has finished.
from concurrent.futures import ThreadPoolExecutor
import time
def work(number):
time.sleep(1)
return number, number * number
with ThreadPoolExecutor(max_workers=4) as executor:
results = executor.map(work, range(10))
for number, square in results:
print(number, square)
The with statement ensures that the executor is shut down correctly
and waits for pending tasks to finish before the block is left.
A Practical Example: Pinging Computers with Threads

A typical use case for threads is waiting for network operations. Suppose we want to determine which IP addresses in a local network respond to a ping request.
Without threads, the hosts could be checked one after another:
import subprocess
def ping(ip):
result = subprocess.run(
["ping", "-c", "1", "-W", "1", ip],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
return result.returncode == 0
for suffix in range(20, 30):
ip = f"192.168.178.{suffix}"
print(f"{ip}: {'alive' if ping(ip) else 'no response'}")
The options -c 1 and -W 1 are used by the common Linux
implementation of ping. Command-line options differ on other operating
systems.
The sequential version waits for the result of each address before checking the next one. This can be unnecessarily slow, especially when hosts do not respond.
With a ThreadPoolExecutor, many ping processes can be waited for
concurrently:
from concurrent.futures import ThreadPoolExecutor
import subprocess
def ping(ip):
result = subprocess.run(
["ping", "-c", "1", "-W", "1", ip],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
return ip, result.returncode == 0
addresses = [
f"192.168.178.{suffix}"
for suffix in range(20, 70)
]
with ThreadPoolExecutor(max_workers=20) as executor:
for ip, alive in executor.map(ping, addresses):
status = "alive" if alive else "no response"
print(f"{ip}: {status}")
This is a good example of a task for which threads are especially useful.
The Python threads themselves perform very little computation. Most of their
time is spent waiting for the external ping processes. While one
thread is waiting, other threads can check other hosts.
A failed ping does not necessarily mean that a computer is switched off. A firewall may block ICMP packets or suppress ping replies.
Summary
- Threads are multiple execution paths within a process.
- Threads share the address space of their process.
- Synchronization mechanisms such as
Lockmay be required when shared mutable data is accessed. threading.Threadis Python's standard interface for individual threads.join()waits for a thread to terminate.ThreadPoolExecutoris convenient for running many similar tasks.- With standard CPython, threads are particularly useful for I/O-bound tasks.
- CPU-bound Python code usually does not become faster with standard CPython threads because of the GIL.
- Free-threaded CPython builds can execute Python threads in parallel, but they require particularly careful handling of shared mutable data.
