How to Create a New Thread in Python
Threads allow your Python programs to perform multiple operations concurrently within a single process. Whether you need to run background tasks, handle I/O-bound operations in parallel, or keep a user interface responsive, understanding how to create and manage threads is essential.
Python's built-in threading module provides two primary ways to create a new thread: by subclassing threading.Thread or by passing a target function to Thread(). This guide covers both approaches with clear examples, explains when to use each, and highlights common pitfalls to avoid.
Understanding Threads in Python
A thread is a lightweight unit of execution that runs within a process. Multiple threads share the same memory space, which makes communication between them easy but also introduces challenges like race conditions.
Python's threading module (part of the standard library) is the go-to tool for working with threads. Before diving into the examples, here are the key concepts you'll encounter:
Threadclass: represents a single thread of execution.start(): begins the thread's activity.run(): the method that contains the code the thread will execute.join(): blocks the calling thread until the thread whosejoin()is called finishes.
Due to Python's Global Interpreter Lock (GIL), threads do not achieve true parallelism for CPU-bound tasks. However, they are highly effective for I/O-bound workloads such as network requests, file operations, and database queries.
Method 1: Creating a Thread by Subclassing threading.Thread
You can create a custom thread by defining a class that inherits from threading.Thread and overriding its run() method. This approach is ideal when your thread encapsulates its own state and behavior.
import threading
class MyThread(threading.Thread):
def __init__(self, thread_name, thread_id):
threading.Thread.__init__(self)
self.thread_name = thread_name
self.thread_id = thread_id
def run(self):
print(f"{self.thread_name} (ID: {self.thread_id}) is running")
# Create thread instances
thread1 = MyThread("Worker-A", 1000)
thread2 = MyThread("Worker-B", 2000)
# Start threads
thread1.start()
thread2.start()
# Wait for both threads to finish
thread1.join()
thread2.join()
print("Both threads have finished. Exiting.")
Output:
Worker-A (ID: 1000) is running
Worker-B (ID: 2000) is running
Both threads have finished. Exiting.
How It Works
- Subclass
threading.Thread:MyThreadinherits fromThread, making each instance a thread object. - Override
__init__(): call the parent constructor withthreading.Thread.__init__(self)(orsuper().__init__()), then store any custom attributes. - Override
run(): define the code that the thread should execute when started. - Call
start(): this internally invokes yourrun()method in a new thread. - Call
join(): ensures the main program waits for the threads to complete before moving on.
Always call start(), never call run() directly. Calling run() executes the method in the current thread instead of spawning a new one:
# ❌ Wrong: runs in the main thread, not a new thread
thread1.run()
# ✅ Correct: spawns a new thread
thread1.start()
Method 2: Creating a Thread Using a Target Function
If your thread logic is simple and doesn't require maintaining state, you can pass a regular function as the target argument to threading.Thread(). This is the quickest and most common approach.
from threading import Thread
from time import sleep
def threaded_function(count, label):
for i in range(count):
print(f"{label}: iteration {i + 1}")
sleep(0.5)
if __name__ == "__main__":
thread = Thread(target=threaded_function, args=(5, "Worker"))
thread.start()
thread.join()
print("Thread finished. Exiting.")
Output:
Worker: iteration 1
Worker: iteration 2
Worker: iteration 3
Worker: iteration 4
Worker: iteration 5
Thread finished. Exiting.
How It Works
- Define a function:
threaded_functioncontains the logic to execute concurrently. - Create a
Threadinstance: pass the function viatargetand its arguments viaargs(a tuple). - Call
start(): launches the function in a separate thread. - Call
join(): blocks until the thread completes.
Use the args parameter for positional arguments and kwargs for keyword arguments:
thread = Thread(
target=threaded_function,
args=(5,),
kwargs={"label": "Worker"}
)
Running Multiple Threads Concurrently
In practice, you often need to launch several threads at once and wait for all of them to finish. Here's a clean pattern for doing that:
from threading import Thread
from time import sleep
def download_file(file_name, duration):
print(f"Starting download: {file_name}")
sleep(duration) # Simulate download time
print(f"Finished download: {file_name}")
files = [
("report.pdf", 2),
("image.png", 1),
("data.csv", 3),
]
threads = []
for file_name, duration in files:
t = Thread(target=download_file, args=(file_name, duration))
threads.append(t)
t.start()
# Wait for all threads to complete
for t in threads:
t.join()
print("All downloads complete.")
Output:
Starting download: report.pdf
Starting download: image.png
Starting download: data.csv
Finished download: image.png
Finished download: report.pdf
Finished download: data.csv
All downloads complete.
Notice that image.png finishes first because its simulated download time is the shortest: the threads run concurrently.
Common Mistake: Forgetting join()
A frequent error is omitting join(), which causes the main program to continue (or exit) before the threads have finished:
from threading import Thread
from time import sleep
def slow_task():
sleep(2)
print("Task complete")
thread = Thread(target=slow_task)
thread.start()
# ❌ No join(): the message below may print before "Task complete"
print("Main program exiting")
Possible output:
Main program exiting
Task complete
The fix is simple: call join() before any code that depends on the thread's result:
thread = Thread(target=slow_task)
thread.start()
thread.join() # ✅ Wait for the thread to finish
print("Main program exiting")
Output:
Task complete
Main program exiting
If you want a thread to automatically terminate when the main program exits (without calling join()), set it as a daemon thread:
thread = Thread(target=slow_task, daemon=True)
thread.start()
Daemon threads are killed when no non-daemon threads are left alive. Use them for background tasks that don't need to complete before the program exits.
Class-Based vs. Function-Based Threads: When to Use Which
| Criteria | Class-Based (Subclassing) | Function-Based (target) |
|---|---|---|
| Complexity | Better for complex, stateful threads | Best for simple, stateless tasks |
| Reusability | Easy to extend and reuse | Requires separate state management |
| Readability | Clear structure for large thread logic | Concise for short operations |
| Setup effort | More boilerplate | Minimal setup |
Rule of thumb: Use the function-based approach for quick, straightforward tasks. Switch to the class-based approach when your thread needs to maintain internal state, perform complex logic, or be reused across your codebase.
Conclusion
Creating threads in Python is straightforward with the threading module:
- Subclass
threading.Threadand overriderun()when you need encapsulated, reusable thread logic with internal state. - Pass a function to
Thread(target=...)for quick, simple concurrency with minimal boilerplate.
Regardless of the method you choose, remember to call start() to launch the thread and join() to wait for it to complete. For I/O-bound workloads, such as network requests, file downloads, or database queries, threads can significantly improve your program's responsiveness and throughput.