Python
threading
multithreading
concurrency
programming

Creating Threads in python

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Threads in Python are useful when work spends time waiting on I O, such as network requests, disk access, or external services. Creating a thread is straightforward with the threading module, but using threads well requires a clear idea of startup, shared state, and shutdown.

The Basic Thread API

The standard pattern is:

  1. define a target function
  2. create a Thread
  3. call start()
  4. call join() when you need to wait for completion
python
1import threading
2import time
3
4
5def worker(name, delay):
6    print(f"{name} started")
7    time.sleep(delay)
8    print(f"{name} finished")
9
10
11thread = threading.Thread(target=worker, args=("task-1", 1))
12thread.start()
13thread.join()
14print("main finished")

start() launches the thread. join() blocks until it finishes. Without join(), the main program may continue before you expect.

Running Multiple Threads

If you have several independent I O-bound tasks, start several threads and then join them all.

python
1import threading
2import time
3
4
5def fetch(task_id):
6    time.sleep(0.2)
7    print(f"task {task_id} done")
8
9
10threads = []
11for i in range(5):
12    t = threading.Thread(target=fetch, args=(i,), name=f"worker-{i}")
13    t.start()
14    threads.append(t)
15
16for t in threads:
17    t.join()

This pattern is fine for small batches of work and helps keep network-heavy programs responsive.

Shared Data Requires Synchronization

Threads become dangerous when they modify shared mutable state without coordination. Even simple increments can race.

python
1import threading
2
3counter = 0
4lock = threading.Lock()
5
6
7def increment(times):
8    global counter
9    for _ in range(times):
10        with lock:
11            counter += 1
12
13
14threads = [threading.Thread(target=increment, args=(10000,)) for _ in range(4)]
15for t in threads:
16    t.start()
17for t in threads:
18    t.join()
19
20print(counter)

The lock protects the critical section so the final count stays correct.

Daemon Threads vs Regular Threads

Python threads can be marked as daemon threads:

python
thread = threading.Thread(target=worker, args=("background", 5), daemon=True)
thread.start()

Daemon threads are stopped abruptly when the main program exits. They are useful for nonessential background activity, but they are a poor choice for work that must flush files, finish network operations, or release resources cleanly.

For most application logic, regular threads plus explicit shutdown are safer.

Graceful Shutdown with Event

Long-running threads should listen for a stop signal.

python
1import threading
2import time
3
4stop_event = threading.Event()
5
6
7def run_loop():
8    while not stop_event.is_set():
9        print("working")
10        time.sleep(0.3)
11
12
13thread = threading.Thread(target=run_loop)
14thread.start()
15
16time.sleep(1)
17stop_event.set()
18thread.join()
19print("stopped cleanly")

This is much better than killing the process and hoping the thread was in a safe place to stop.

When to Use a Thread Pool Instead

If you need to run many similar tasks, ThreadPoolExecutor is often cleaner than managing raw Thread objects yourself.

python
1from concurrent.futures import ThreadPoolExecutor
2import time
3
4
5def work(item):
6    time.sleep(0.1)
7    return item * 2
8
9
10with ThreadPoolExecutor(max_workers=4) as pool:
11    results = list(pool.map(work, [1, 2, 3, 4, 5]))
12
13print(results)

Thread pools are easier to scale, easier to collect results from, and easier to use for batches of short jobs.

The GIL Question

A common misunderstanding is that threads automatically speed up every kind of Python work. They do not. For CPU-heavy pure-Python computation, the GIL limits parallel execution of Python bytecode.

Practical rule:

  • I O-bound work: threads are a good fit
  • CPU-bound work: prefer multiprocessing or native libraries
  • huge numbers of socket tasks: consider asyncio

Choosing the right concurrency model matters more than merely "using threads."

Common Pitfalls

Starting a thread and forgetting to join() it can make program flow look random, especially in short scripts.

Sharing mutable data without locks or queues introduces race conditions that are often intermittent and hard to reproduce.

Using daemon threads for critical work can lose data because the interpreter may exit before cleanup finishes.

Expecting threads to speed up CPU-bound Python code usually leads to disappointment because of the GIL.

Summary

  • Create Python threads with threading.Thread, then start() and join() them.
  • Threads are most useful for I O-bound tasks.
  • Protect shared mutable state with synchronization primitives like Lock.
  • Use Event for graceful shutdown of long-running workers.
  • For large batches of similar work, ThreadPoolExecutor is often cleaner than manual thread management.

Course illustration
Course illustration

All Rights Reserved.