- Function Target — simplest way to create threads
- Subclassing Thread — object-oriented approach
- Thread Pool Executor — managing many threads
- Thread Lifecycle — from creation to completion
- When to use each method — choosing the right approach
Why Create Threads?
Creating threads allows your program to do multiple things at once. It's like having multiple workers in a factory instead of just one.
🏭 Think of it like a factory assembly line.
Without threads: One worker does everything — takes parts, assembles, packages. Slow and inefficient.
With threads: Multiple workers do different tasks simultaneously — one takes parts, another assembles, another packages. Fast and efficient!
💡 Key concept: Creating a thread is like hiring a new worker. Each thread can do its own task independently.
Ways to Create Threads
# ============================================================
# THREE WAYS TO CREATE THREADS
# ============================================================
print("""
┌─────────────────────────────────────────────────────────────────┐
│ METHOD 1: FUNCTION TARGET │
│ - Simplest and most common │
│ - Pass a function to Thread constructor │
│ - Good for simple tasks │
├─────────────────────────────────────────────────────────────────┤
│ METHOD 2: SUBCLASSING THREAD │
│ - Create a class that inherits from Thread │
│ - Override the run() method │
│ - Good for complex threads with state │
├─────────────────────────────────────────────────────────────────┤
│ METHOD 3: THREAD POOL EXECUTOR │
│ - Manage multiple threads efficiently │
│ - Reuses threads for many tasks │
│ - Best for many small tasks │
└─────────────────────────────────────────────────────────────────┘
""")
Key point: Choose the method based on your needs. Function target is simplest; subclassing gives more control; thread pools are efficient for many tasks.
Quick Check: What are the three ways to create threads in Python? (Answer: Function target, subclassing Thread, and Thread Pool Executor)
Method 1: Function Target
The Simplest Way to Create Threads
# ============================================================
# METHOD 1: FUNCTION TARGET
# ============================================================
import threading
import time
# ============================================================
# BASIC EXAMPLE
# ============================================================
def print_numbers():
"""Print numbers from 1 to 5"""
for i in range(1, 6):
print(f"Number: {i}")
time.sleep(0.3)
def print_letters():
"""Print letters from A to E"""
for letter in ['A', 'B', 'C', 'D', 'E']:
print(f"Letter: {letter}")
time.sleep(0.3)
# Create threads
thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_letters)
# Start threads
thread1.start()
thread2.start()
# Wait for threads to finish
thread1.join()
thread2.join()
print("Both threads finished!")
# ============================================================
# THREAD WITH ARGUMENTS
# ============================================================
def greet(name, times, delay=0.5):
"""Greet someone multiple times"""
for i in range(times):
print(f"Hello, {name}! (Greeting {i+1})")
time.sleep(delay)
# Create thread with arguments
thread = threading.Thread(
target=greet,
args=("Alice", 3),
kwargs={"delay": 0.3}
)
thread.start()
thread.join()
# ============================================================
# THREAD WITH RETURN VALUE (Using a list)
# ============================================================
def calculate_square(n, result_list, index):
"""Calculate square and store in list"""
result = n * n
result_list[index] = result
print(f"Square of {n} is {result}")
results = [0] * 5
threads = []
for i in range(5):
t = threading.Thread(target=calculate_square, args=(i+1, results, i))
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"Results: {results}")
Function target key points:
- Simple — just pass a function
- args — pass positional arguments
- kwargs — pass keyword arguments
- No return — threads don't return values directly
Quick Check: How do you pass arguments to a thread created with function target? (Answer: Using args and kwargs parameters)
Method 2: Subclassing Thread
Object-Oriented Thread Creation
# ============================================================
# METHOD 2: SUBCLASSING THREAD
# ============================================================
import threading
import time
# ============================================================
# BASIC SUBCLASS
# ============================================================
class MyThread(threading.Thread):
"""A custom thread class"""
def __init__(self, name, delay):
threading.Thread.__init__(self)
self.name = name
self.delay = delay
def run(self):
"""Code to run in the thread"""
print(f"Thread {self.name} starting...")
time.sleep(self.delay)
print(f"Thread {self.name} finished!")
# Create and start threads
t1 = MyThread("A", 2)
t2 = MyThread("B", 3)
t1.start()
t2.start()
t1.join()
t2.join()
# ============================================================
# ADVANCED SUBCLASS WITH STATE
# ============================================================
class WorkerThread(threading.Thread):
"""Worker thread with state and progress tracking"""
def __init__(self, worker_id, task_count):
threading.Thread.__init__(self)
self.worker_id = worker_id
self.task_count = task_count
self.completed = 0
self.is_running = True
def run(self):
"""Process tasks"""
print(f"Worker {self.worker_id} started")
for i in range(self.task_count):
if not self.is_running:
break
# Simulate work
time.sleep(0.5)
self.completed += 1
print(f"Worker {self.worker_id}: {self.completed}/{self.task_count}")
print(f"Worker {self.worker_id} finished ({self.completed} tasks)")
def stop(self):
"""Stop the worker"""
self.is_running = False
# Create workers
workers = []
for i in range(3):
w = WorkerThread(i+1, 5)
workers.append(w)
w.start()
# Let them work
time.sleep(2)
# Stop one worker early
workers[1].stop()
# Wait for all workers
for w in workers:
w.join()
print("All workers finished!")
# ============================================================
# COMPARISON: SUBCLASS vs FUNCTION TARGET
# ============================================================
print("""
┌─────────────────────────────────────────────────────────────────┐
│ SUBCLASSING THREAD: │
│ ✅ More object-oriented │
│ ✅ Can have state and methods │
│ ✅ Better for complex threads │
│ ✅ Can be extended │
├─────────────────────────────────────────────────────────────────┤
│ FUNCTION TARGET: │
│ ✅ Simpler and more Pythonic │
│ ✅ Less boilerplate code │
│ ✅ Good for simple tasks │
│ ✅ Easier to understand │
└─────────────────────────────────────────────────────────────────┘
""")
Subclassing Thread key points:
- run() — override this method
- __init__ — pass custom data
- State — can maintain internal state
- Methods — can have custom methods
Quick Check: What method do you override when subclassing Thread? (Answer: run())
Method 3: Thread Pool Executor
Managing Many Threads Efficiently
# ============================================================
# METHOD 3: THREAD POOL EXECUTOR
# ============================================================
import concurrent.futures
import time
import random
# ============================================================
# BASIC THREAD POOL
# ============================================================
def process_task(task_id):
"""Simulate processing a task"""
delay = random.uniform(0.5, 2.0)
print(f"Task {task_id}: Starting (will take {delay:.2f}s)")
time.sleep(delay)
result = f"Task {task_id} completed in {delay:.2f}s"
print(f"Task {task_id}: Finished")
return result
print("Using ThreadPoolExecutor with 3 workers:")
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
# Submit tasks
futures = []
for i in range(10):
future = executor.submit(process_task, i)
futures.append(future)
# Get results as they complete
for future in concurrent.futures.as_completed(futures):
try:
result = future.result()
print(f"Result: {result}")
except Exception as e:
print(f"Error: {e}")
# ============================================================
# MAP FUNCTION - SIMPLER WAY
# ============================================================
def square(n):
"""Calculate square of a number"""
time.sleep(random.uniform(0.1, 0.3))
return n * n
print("\nUsing map with ThreadPoolExecutor:")
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
numbers = [1, 2, 3, 4, 5, 6, 7, 8]
results = executor.map(square, numbers)
for num, result in zip(numbers, results):
print(f"{num}² = {result}")
# ============================================================
# HANDLING EXCEPTIONS
# ============================================================
def risky_task(n):
"""A task that might fail"""
if n == 5:
raise ValueError(f"Task {n} failed!")
time.sleep(0.1)
return f"Task {n} succeeded"
print("\nHandling exceptions:")
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
futures = {executor.submit(risky_task, i): i for i in range(10)}
for future in concurrent.futures.as_completed(futures):
task_id = futures[future]
try:
result = future.result()
print(f"Task {task_id}: {result}")
except Exception as e:
print(f"Task {task_id}: Failed - {e}")
print("""
┌─────────────────────────────────────────────────────────────────┐
│ THREAD POOL EXECUTOR BENEFITS: │
├─────────────────────────────────────────────────────────────────┤
│ • Reuses threads (no creation overhead) │
│ • Limits the number of threads │
│ • Manages thread lifecycle │
│ • Easy to submit many tasks │
│ • Handles exceptions well │
│ • Clean API (submit, map, as_completed) │
└─────────────────────────────────────────────────────────────────┘
""")
Thread Pool Executor key points:
- Reuses threads — no creation overhead
- Limits threads — prevents overload
- submit() — submit individual tasks
- map() — apply function to iterable
- as_completed() — get results as they finish
Quick Check: What module provides ThreadPoolExecutor? (Answer: concurrent.futures)
Thread Lifecycle
Understanding the Thread Lifecycle
# ============================================================
# THREAD LIFECYCLE
# ============================================================
import threading
import time
class LifecycleDemo:
"""Demonstrate thread lifecycle"""
def __init__(self):
self.running = True
def worker(self):
"""Worker thread demonstrating lifecycle"""
print("1. Thread created (NEW state)")
print("2. Thread starting (RUNNABLE state)")
count = 0
while self.running and count < 5:
count += 1
print(f" Running... ({count})")
time.sleep(0.5)
print("3. Thread completed (TERMINATED state)")
def run_demo(self):
"""Run the lifecycle demo"""
# Create thread (NEW state)
t = threading.Thread(target=self.worker)
print("Thread object created (NEW)")
# Start thread (RUNNABLE state)
t.start()
print("Thread started (RUNNABLE)")
# Check if running
print(f"Is thread alive? {t.is_alive()}")
# Wait for completion
time.sleep(1.5)
# Stop the thread
self.running = False
print("Signaling thread to stop")
# Wait for thread to finish (TERMINATED state)
t.join()
print(f"Is thread alive? {t.is_alive()}")
print("Thread terminated")
# ============================================================
# THREAD STATES
# ============================================================
print("=" * 60)
print("THREAD STATES")
print("=" * 60)
print("""
┌─────────────────────────────────────────────────────────────────┐
│ THREAD STATES │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. NEW │
│ - Thread object created but not started │
│ - t = threading.Thread(target=func) │
│ │
│ 2. RUNNABLE │
│ - Thread is running or ready to run │
│ - t.start() │
│ │
│ 3. BLOCKED │
│ - Thread is waiting for a resource │
│ - Waiting for lock, I/O, sleep, join │
│ │
│ 4. TERMINATED │
│ - Thread has finished execution │
│ - run() method completed │
└─────────────────────────────────────────────────────────────────┘
""")
# Run the demo
print("=" * 60)
print("LIFECYCLE DEMONSTRATION")
print("=" * 60)
demo = LifecycleDemo()
demo.run_demo()
Thread lifecycle key points:
- NEW — created but not started
- RUNNABLE — running or ready to run
- BLOCKED — waiting for resource
- TERMINATED — finished execution
Quick Check: What state is a thread in when it's created but not started? (Answer: NEW)
Method Comparison
Choosing the Right Method
| Feature | Function Target | Subclassing Thread | ThreadPoolExecutor |
|---|---|---|---|
| Simplicity | Very Simple | Moderate | Moderate |
| Code Reuse | Low | High | Medium |
| State Management | Low | High | Medium |
| Many Tasks | Poor | Poor | Excellent |
| Performance | Good | Good | Excellent |
| Best For | Simple tasks | Complex threads | Many tasks |
Recommendations:
- Function Target — for simple, one-off tasks
- Subclassing Thread — for complex, reusable threads
- ThreadPoolExecutor — for many small tasks
Quick Check: Which method is best for many small tasks? (Answer: ThreadPoolExecutor)
Real-World Example
Complete Threaded Application
# ============================================================
# REAL-WORLD: THREADED DATA PROCESSOR
# ============================================================
import threading
import time
import random
import queue
from concurrent.futures import ThreadPoolExecutor
class DataProcessor:
"""Real-world threaded data processor"""
def __init__(self, num_workers=4):
self.num_workers = num_workers
self.task_queue = queue.Queue()
self.results = []
self.lock = threading.Lock()
self.running = False
# ============================================================
# WORKER METHODS
# ============================================================
def worker_function(self, worker_id):
"""Worker using function target"""
print(f"Worker {worker_id}: Starting")
while self.running:
try:
# Get task from queue
task = self.task_queue.get(timeout=1)
if task is None:
break
# Process task
result = self.process_task(worker_id, task)
# Store result
with self.lock:
self.results.append(result)
self.task_queue.task_done()
except queue.Empty:
continue
print(f"Worker {worker_id}: Stopping")
def process_task(self, worker_id, task):
"""Process a single task"""
print(f"Worker {worker_id}: Processing {task}")
time.sleep(random.uniform(0.2, 0.6))
return f"Processed {task} by worker {worker_id}"
# ============================================================
# THREAD CREATION METHODS
# ============================================================
def create_workers_function_target(self):
"""Create workers using function target"""
threads = []
for i in range(self.num_workers):
t = threading.Thread(
target=self.worker_function,
args=(i,),
name=f"Worker-{i}"
)
threads.append(t)
t.start()
return threads
def create_workers_subclass(self):
"""Create workers using subclassing"""
class WorkerThread(threading.Thread):
def __init__(self, processor, worker_id):
threading.Thread.__init__(self)
self.processor = processor
self.worker_id = worker_id
self.name = f"Worker-{worker_id}"
def run(self):
self.processor.worker_function(self.worker_id)
threads = []
for i in range(self.num_workers):
t = WorkerThread(self, i)
threads.append(t)
t.start()
return threads
def create_workers_pool(self):
"""Create workers using ThreadPoolExecutor"""
executor = ThreadPoolExecutor(max_workers=self.num_workers)
futures = []
for i in range(self.num_workers):
future = executor.submit(self.worker_function, i)
futures.append(future)
return executor, futures
# ============================================================
# MAIN APPLICATION
# ============================================================
def run(self, num_tasks=20, method="function"):
"""Run the data processor"""
print("=" * 60)
print(f"DATA PROCESSOR - Method: {method}")
print("=" * 60)
self.running = True
self.results = []
# Add tasks to queue
for i in range(num_tasks):
self.task_queue.put(f"Task-{i+1}")
print(f"Added {num_tasks} tasks to queue")
# Start workers based on method
if method == "function":
threads = self.create_workers_function_target()
elif method == "subclass":
threads = self.create_workers_subclass()
elif method == "pool":
executor, futures = self.create_workers_pool()
else:
print("Invalid method")
return
# Wait for tasks to complete
self.task_queue.join()
# Stop workers
self.running = False
if method == "pool":
executor.shutdown()
else:
for t in threads:
t.join()
print(f"\nCompleted {len(self.results)} tasks")
print("Results:")
for result in self.results[:5]:
print(f" {result}")
if len(self.results) > 5:
print(f" ... and {len(self.results)-5} more")
# ============================================================
# RUN THE APPLICATION
# ============================================================
processor = DataProcessor(num_workers=3)
# Test all three methods
processor.run(num_tasks=10, method="function")
print("\n" + "=" * 60)
print("METHOD COMPARISON SUMMARY")
print("=" * 60)
print("""
┌─────────────────────────────────────────────────────────────────┐
│ METHOD │ BEST FOR │
├─────────────────────────────────────────────────────────────────┤
│ Function Target │ Simple, one-off tasks │
│ Subclassing │ Complex threads with state │
│ ThreadPool │ Many small tasks, efficient │
└─────────────────────────────────────────────────────────────────┘
Choose the method that best fits your needs!
""")
Real-world example key points:
- Queue-based — tasks are queued and processed
- Multiple methods — demonstrates all three approaches
- Thread-safe — uses locks for shared data
- Scalable — can handle many tasks
Quick Check: What is the advantage of using a queue with threads? (Answer: It provides thread-safe communication between threads)
Best Practices
Thread Creation Best Practices
# ============================================================
# THREAD CREATION BEST PRACTICES
# ============================================================
print("1. USE THE RIGHT METHOD")
print(" - Function target for simple tasks")
print(" - Subclassing for complex threads")
print(" - ThreadPoolExecutor for many tasks")
print("\n2. ALWAYS JOIN THREADS")
print(" - Ensure threads complete before program exits")
print(" - Prevents resource leaks")
print(" - Use try/finally for safety")
print("\n3. NAME YOUR THREADS")
print(" - Use meaningful names")
print(" - Makes debugging easier")
print(" - thread.name = 'Worker-1'")
print("\n4. USE DAEMON THREADS CAREFULLY")
print(" - Use for background tasks")
print(" - Don't use for critical work")
print(" - Set t.daemon = True before start")
print("\n5. HANDLE EXCEPTIONS")
print(" - Exceptions in threads don't propagate")
print(" - Use try/except inside thread functions")
print(" - Log errors for debugging")
print("\n6. USE QUEUES FOR COMMUNICATION")
print(" - from queue import Queue")
print(" - Thread-safe and easy to use")
print(" - Avoid shared variables without locks")
print("\n7. LIMIT THREAD COUNT")
print(" - Too many threads = performance issues")
print(" - Use ThreadPoolExecutor for limits")
print(" - Max workers = 2 * CPU cores for CPU tasks")
print("\n8. USE LOCKS FOR SHARED DATA")
print(" - from threading import Lock")
print(" - Lock before reading/writing shared data")
print(" - Unlock immediately after")
Best practices summary:
- Choose right method — based on task complexity
- Always join — prevent resource leaks
- Name threads — easier debugging
- Use queues — thread-safe communication
Quick Check: What should you use for thread-safe communication? (Answer: Queue)
Try It Yourself
Experiment with creating threads in the editor below.
CREATING THREADS - PRACTICE
========================================
1. FUNCTION TARGET METHOD
----------------------------------------
Starting: Thread-1
Thread-1: Working...
Starting: Thread-2
Thread-2: Working...
Starting: Thread-3
Thread-3: Working...
Completed: Thread-3
Completed: Thread-1
Completed: Thread-2
Joined: Thread-1
Joined: Thread-2
Joined: Thread-3
Result 1: Thread-1 completed
Result 2: Thread-2 completed
Result 3: Thread-3 completed
2. SUBCLASSING METHOD
----------------------------------------
Starting: Worker-A
Worker-A: Running task 1
Starting: Worker-B
Worker-B: Running task 2
Completed: Worker-A
Completed: Worker-B
Joined: Worker-A
Joined: Worker-B
Result A: Task 1 done
Result B: Task 2 done
3. THREAD POOL METHOD
----------------------------------------
Pool task 0: Working...
Pool task 1: Working...
Pool task 2: Working...
Pool task 3: Working...
Pool task 4: Working...
4. THREAD CREATION SUMMARY
----------------------------------------
┌─────────────────────────────────────────────────────────────────┐
│ METHOD │ WHEN TO USE │
├─────────────────────────────────────────────────────────────────┤
│ Function Target │ Simple tasks, quick and easy │
│ Subclassing Thread │ Complex threads with state │
│ ThreadPoolExecutor │ Many small tasks, efficient │
└─────────────────────────────────────────────────────────────────┘
Three ways to create threads, choose the right one!
You've Got It!
You now know how to create threads in Python using three different methods. You understand when to use each method and the thread lifecycle.
Quick Quiz
Test what you've learned:
Frequently Asked Questions
What is the difference between function target and subclassing Thread?
Why use ThreadPoolExecutor?
What is a common interview question about creating threads?
Can a thread return a value?
What is the thread lifecycle?
Where to Go From Here
Now that you know how to create threads, check out these related topics:
Single Tasking
Learn about single-threaded execution.
Learn More →Multi Tasking
Learn about concurrent execution.
Learn More →Thread Synchronization
Learn how to safely share data between threads.
Learn More →