Skip to main content

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:

  • Thread class: 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 whose join() is called finishes.
info

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

  1. Subclass threading.Thread: MyThread inherits from Thread, making each instance a thread object.
  2. Override __init__(): call the parent constructor with threading.Thread.__init__(self) (or super().__init__()), then store any custom attributes.
  3. Override run(): define the code that the thread should execute when started.
  4. Call start(): this internally invokes your run() method in a new thread.
  5. Call join(): ensures the main program waits for the threads to complete before moving on.
caution

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

  1. Define a function: threaded_function contains the logic to execute concurrently.
  2. Create a Thread instance: pass the function via target and its arguments via args (a tuple).
  3. Call start(): launches the function in a separate thread.
  4. Call join(): blocks until the thread completes.
tip

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
tip

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

CriteriaClass-Based (Subclassing)Function-Based (target)
ComplexityBetter for complex, stateful threadsBest for simple, stateless tasks
ReusabilityEasy to extend and reuseRequires separate state management
ReadabilityClear structure for large thread logicConcise for short operations
Setup effortMore boilerplateMinimal 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.Thread and override run() 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.