Skip to main content

Python Threading: How to Create Threads (2026)

Python's threading module provides the Thread class for spawning and managing threads. To create a thread, you pass a callable (a function or a lambda) to Thread(), call start() to launch it, and use join() to wait for completion. Daemon threads run in the background and are terminated when the main program exits, while non-daemon threads must finish before the program closes.

Thread creation is deceptively simple, but correctness requires understanding thread lifecycles, cleanup, and the relationship between daemon and non-daemon threads. I've debugged countless applications where threads silently leak or fail to shut down cleanly—the root cause is always incomplete lifecycle management. This article covers every pattern you need.

The Basic Thread Creation Pattern

The simplest way to run a function in a thread is to create a Thread object, pass your function as the target parameter, and call start():

import threading

def worker(name):
"""A simple function to run in a thread."""
print(f"Thread {name} started")
# ... do work here ...
print(f"Thread {name} finished")

# Create a thread
thread = threading.Thread(target=worker, args=("A",))

# Start execution (does NOT block)
thread.start()

# Wait for the thread to finish
thread.join()

print("All work complete")

When you call start(), Python spawns a new thread and begins executing your function. The start() call returns immediately—it does not block the caller. To wait for a thread to finish, call join(). The program blocks at join() until that thread completes.

Using Thread Subclasses

For more complex scenarios, you can subclass Thread and override the run() method:

import threading
import time

class DownloadWorker(threading.Thread):
"""A thread subclass that downloads a file."""

def __init__(self, url, filename):
"""Initialize the thread with custom arguments."""
super().__init__()
self.url = url
self.filename = filename
self.success = False

def run(self):
"""This method is called when the thread starts."""
try:
print(f"Downloading {self.url} to {self.filename}")
# Simulate download (real code uses requests or urllib)
time.sleep(2)
self.success = True
print(f"Successfully downloaded {self.filename}")
except Exception as e:
print(f"Failed to download {self.url}: {e}")
self.success = False

# Create and start multiple worker threads
downloads = [
DownloadWorker("https://example.com/file1.zip", "file1.zip"),
DownloadWorker("https://example.com/file2.zip", "file2.zip"),
]

for thread in downloads:
thread.start()

# Wait for all to finish
for thread in downloads:
thread.join()

# Check results
results = [t.success for t in downloads]
print(f"Completed {sum(results)}/{len(results)} downloads")

The subclass pattern is useful when you need to store state per thread or pass complex initialization data. Each thread instance is independent, so you can safely store instance variables like self.success without worrying about cross-thread contamination.

Daemon Threads vs. Non-Daemon Threads

By default, threads are non-daemon. The Python process will wait for all non-daemon threads to finish before exiting. Daemon threads are "background" threads that the process will terminate when all non-daemon threads finish, even if the daemon threads are still running.

import threading
import time

def background_logger():
"""A daemon thread that logs indefinitely."""
count = 0
while True:
print(f"Log entry #{count}")
count += 1
time.sleep(1)

def main():
"""Main program with a daemon thread."""
# Create a daemon thread
logger = threading.Thread(target=background_logger, daemon=True)
logger.start()

# Main program does some work
print("Main program working...")
time.sleep(3)
print("Main program done")
# When main exits, Python terminates the daemon thread
# without waiting for it to finish

if __name__ == "__main__":
main()

In the example above, the daemon thread logs "Log entry #0", "#1", "#2", and possibly "#3" before the program exits. It never finishes its infinite loop—Python kills it when the main program ends.

Use daemon threads for logging, periodic health checks, and background housekeeping that isn't critical to the application's core logic. Use non-daemon threads for work that must be completed before the application shuts down, such as database commits or cleanup operations.

Passing Arguments and Keyword Arguments

The Thread constructor accepts args (a tuple of positional arguments) and kwargs (a dictionary of keyword arguments) to pass to the target function:

import threading
import time

def process_batch(batch_id, items, output_dir="/tmp"):
"""Process a batch of items and write results."""
print(f"Processing batch {batch_id} with {len(items)} items")
result_file = f"{output_dir}/batch_{batch_id}.txt"
# Simulate processing
time.sleep(1)
print(f"Batch {batch_id} written to {result_file}")

# Launch three threads with different arguments
threads = []
for i in range(3):
items = [f"item_{j}" for j in range(5)]
thread = threading.Thread(
target=process_batch,
args=(i, items),
kwargs={"output_dir": "/var/log/results"}
)
threads.append(thread)
thread.start()

# Wait for all threads
for t in threads:
t.join()

print("All batches processed")

This pattern is ideal for worker pools where each thread receives different data.

Thread Naming and Identification

Every thread has a name (useful for debugging) and an identifier. You can set the name when creating the thread or read it later:

import threading

def worker():
print(f"Running in thread: {threading.current_thread().name}")
print(f"Thread ID: {threading.current_thread().ident}")

thread1 = threading.Thread(target=worker, name="Worker-1")
thread2 = threading.Thread(target=worker, name="Worker-2")

thread1.start()
thread2.start()
thread1.join()
thread2.join()

# Also list all active threads
print(f"Active threads: {threading.enumerate()}")

Thread names are assigned automatically (Thread-1, Thread-2, etc.) if you don't provide one, but descriptive names are invaluable in logs and debuggers.

Comparison Table: Thread Creation Methods

MethodUse CaseComplexityState Management
Simple target=functionQuick background tasksVery lowPass via args/kwargs
Thread subclass with run()Complex workers with multiple methodsMediumStore in instance variables
Daemon threadsBackground logging, health checksLowTerminated automatically
Named threadsDebugging, monitoringVery lowSet name parameter

Key Takeaways

  • Threads are created with Thread(target=function), started with start(), and joined with join().
  • Daemon threads are background threads that Python terminates when the main program exits; non-daemon threads must finish first.
  • Thread subclasses allow per-thread state and complex initialization via __init__() and run().
  • Arguments are passed via args (tuple) and kwargs (dict).
  • Thread names and identifiers aid in debugging and monitoring multi-threaded programs.

Frequently Asked Questions

Can I start a thread multiple times?

No. Once you call start(), the thread runs and cannot be restarted. To run the same function again, create a new Thread object and call start() on it.

What's the maximum number of threads I can create?

There's no hard limit in Python, but the OS has limits. On Linux, the default is often 1024 threads per process (checked via ulimit -u). In practice, overhead becomes the bottleneck: each thread costs about 1-8 MB of memory and scheduler overhead. For thousands of concurrent tasks, use asyncio or ThreadPoolExecutor instead of raw threads.

How do I stop a thread?

There's no built-in way to forcefully terminate a thread in Python. Instead, use a flag that the thread checks periodically. See the section on "Avoiding Common Threading Pitfalls" for a clean shutdown pattern.

Is it safe to call join() on a thread that hasn't started?

Yes, join() will return immediately if the thread hasn't started yet. However, it's generally good practice to call start() before join() to avoid confusion.

Can I pass mutable objects to a thread?

Yes, but you must synchronize access using locks (see the next article). If two threads modify the same list or dict without coordination, the data becomes corrupted.

Further Reading