Have you ever run a script and suddenly had the interface stop responding or the terminal appear frozen? This is extremely common among developers moving beyond the basics. By default, Python executes instructions sequentially: one after another. If a task takes a long time (downloading a large file, processing thousands of records), everything else must wait. This is where Python threading comes in, a technique for running multiple tasks simultaneously and preventing your applications from locking up.
Why does your script freeze?
Imagine following a cake recipe: you mix the batter, put it in the oven, and then stand in front of the oven for 40 minutes, unable to wash dishes or set the table. In programming, this is called synchronous or sequential execution. When a script needs to read a large file or make a web request, it sends the request and “blocks,” waiting for the response. During that wait, the processor is idle, but your program appears frozen to the user.
What is threading in Python?
A “thread” is the smallest unit of processing that can be managed by the operating system. Python threading lets your program execute multiple threads within the same process. Back to the kitchen: while the cake is baking, you can start washing the dishes. You are still one person (one process), but managing multiple tasks by rapidly alternating between them.
Threading is different from multiprocessing. Threading shares the same memory space; multiprocessing uses multiple CPU cores truly in parallel. Python has the Global Interpreter Lock (GIL), which ensures only one thread executes Python code at a time. This sounds like a limitation, but for tasks involving waiting (network, disk), threading is highly efficient. For a deep dive on the GIL, see Python GIL explained.
Practical example: simulating downloads without freezing
import threading
import time
def download_file(name):
print(f"Starting download of {name}...")
time.sleep(3) # Simulating a slow task
print(f"Download of {name} complete!")
# Create threads
t1 = threading.Thread(target=download_file, args=("Report.pdf",))
t2 = threading.Thread(target=download_file, args=("Image.png",))
# Start threads
t1.start()
t2.start()
print("Main program continues running!")
# Wait for threads to finish
t1.join()
t2.join()
print("All downloads complete.")The .join() method tells the main program: “wait here until this thread finishes before continuing.” Without it, the script might exit before the downloads complete. The official Python threading documentation covers all available classes and methods.
Preventing GUI freezes
One of the biggest uses of threading is in GUI applications. If you run a heavy function directly on a button click in Tkinter, the window’s MainLoop pauses and the window shows “Not Responding.” Always fire heavy logic in a separate thread so the MainLoop keeps running smoothly while processing happens in the background.
Race conditions and Lock
When multiple threads try to modify the same variable simultaneously, a race condition can corrupt data. Use a Lock object to prevent this: when one thread is accessing shared data, it “locks” access so other threads must wait.
import threading
lock = threading.Lock()
counter = 0
def increment():
global counter
with lock: # Only one thread enters this block at a time
counter += 1
threads = [threading.Thread(target=increment) for _ in range(1000)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Final counter: {counter}") # Always 1000ThreadPoolExecutor: the cleaner approach
For managing many threads, use concurrent.futures.ThreadPoolExecutor instead of creating threads manually. It manages a pool of threads for you, reusing them and handling cleanup automatically:
from concurrent.futures import ThreadPoolExecutor
def process_item(item):
# Simulate work
return item * 2
items = [1, 2, 3, 4, 5]
with ThreadPoolExecutor(max_workers=3) as executor:
results = list(executor.map(process_item, items))
print(results) # [2, 4, 6, 8, 10]Best practices summary
- Use ThreadPoolExecutor for managing multiple threads instead of manual creation.
- Daemon threads: Set
daemon=Truefor background monitor threads that should die when the main program exits. - Avoid CPU-bound tasks: For heavy calculations, threading does not help due to the GIL. Use
multiprocessingor NumPy instead. - Use logging over print: Python’s logging module is thread-safe and identifies which thread produced each message.
Frequently asked questions
Does threading make Python faster?
For I/O-bound tasks (waiting), yes. For CPU-bound tasks (calculations), no, due to the GIL.
What is the difference between start() and run()?
start() launches the function in a new thread. run() executes the function in the current thread sequentially.
What happens if I forget join()?
The main program continues its execution. Non-daemon threads will keep the process alive until they finish.
How do I stop a thread forcefully?
The threading module does not provide a safe way to kill a thread. Use a control variable (flag) that the thread itself checks to exit the loop.






