Introduction to multithreading in Python
By Flavio
Published · Updated
Tagged: Python
An introduction to multithreading in Python: using the threading module and Thread() to run a slow function on its own thread while your program continues.
A Python application runs on a single thread, unless you explicitly enable multithreading.
Why is multithreading useful? Code in Python runs in sequence, one instruction after another.
If you define a function that sleeps 3 seconds and then prints something, like this:
import time
def greet():
time.sleep(3)
print('hello')
greet()
print('world')
The world string is printed after 3 seconds, because we call time.sleep(3) inside the greet() function.
This is just a silly example, but imagine getting a resource from the network, or writing a big file to disk. Anything that waits on I/O.
With multithreading we can run that wait on a separate thread, and go on with our program in the meantime.
The threading standard library module helps with implementing multithreading. You import Thread from it:
from threading import Thread
Then we pass the function we must execute as the target argument to the Thread class constructor, getting a thread object:
t = Thread(target=greet)
then we call its start() method to start the thread:
t.start()
Pass arguments with args. It is a tuple. A one-item tuple needs the trailing comma:
def greet(name):
time.sleep(3)
print(name)
t = Thread(target=greet, args=('Roger',))
t.start()
Do not write Thread(target=greet('Roger')). That calls greet on this thread, then passes None to target.
Try running this code:
from threading import Thread
import time
def greet():
time.sleep(3)
print('hello')
t = Thread(target=greet)
t.start()
print('world')
You will now see world printed 3 seconds before hello shows up in the console.
Call t.join() if you need to wait for the thread to finish before you continue.
The program does not end until the thread (or all the threads it started) end, unless you start a thread as a daemon.
concurrent.futures.ThreadPoolExecutor is the higher-level option when you want a pool of threads.
The GIL
In the standard CPython build only one thread runs Python bytecode at a time. That lock is the Global Interpreter Lock, the GIL.
Threads help with I/O-bound work: network, disk, sleeping, like the example above. They do not help CPU-bound work like processing an image. For that, use multiprocessing or concurrent.futures.ProcessPoolExecutor, or a free-threaded build.
Free-threaded CPython was experimental in Python 3.13 (PEP 703) and officially supported since Python 3.14 (PEP 779). Python 3.15 is the latest stable, and it still ships that build as a separate install, not the default. You run it as python3.15t.
Two threads that update the same variable can lose updates. count += 1 is a read, then an add, then a write. Another thread can sneak in between:
from threading import Thread
count = 0
def add():
global count
for _ in range(100000):
count += 1
first = Thread(target=add)
second = Thread(target=add)
first.start()
second.start()
first.join()
second.join()
print(count)
You expect 200000. The result can be lower, because another thread can sneak in between the read and the write. Use a Lock around the update, or do not share that state.
Those are the basics of multithreading. This is complex, and can lead to the introduction of bugs if not done well.
Want me to talk about your product? You can sponsor this site.