CHAPTER 42 · ADVANCED4 min read

Threads and I/O

Overlap waiting work while keeping state, ordering, and failure handling explicit.

Python 3.12+Free to readModule 10 of 13
YOUR NEXT SMALL WINS

By the end, you’ll be able to…

  • Identify I/O-bound tasks suitable for a thread pool.
  • Collect futures without losing failures.
  • Protect shared invariants rather than assuming the GIL provides safety.

01Concurrency is overlapping progress

An I/O-bound task spends substantial time waiting for a file, socket, database, or other external resource. Threads can let another task progress during that wait. Concurrency describes overlapping lifetimes; parallelism means simultaneous execution. A thread pool can improve throughput without making each individual request faster, and it can make a remote service slower if it sends too much work at once.

In a conventional CPython build, the global interpreter lock generally permits only one thread to execute Python bytecode at a time. Many blocking I/O operations release it. CPU-heavy pure Python therefore usually does not gain parallel throughput from threads on that build. Some native extensions release the GIL, and optional free-threaded CPython builds starting with Python 3.13 change the execution model; verify the actual runtime and workload.

02A bounded pool owns worker lifetime

ThreadPoolExecutor manages a finite number of worker threads. Its context manager waits for submitted work during shutdown, so an exception in the caller does not automatically make every running operation disappear. Give blocking operations their own timeouts. The demonstration uses a short sleep as a local stand-in for waiting and does not contact any external service.

executor.map returns results in input order even if tasks finish in another order. This is convenient for ordered transformations, but a slow early task can delay observing later results. submit plus as_completed lets you process completed futures promptly. Always retrieve each future’s result: that is where a worker’s exception becomes visible to the caller.

thread_pool.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

03Shared state needs an invariant

The GIL is not a substitute for application synchronization. A read-check-write sequence can be interrupted between steps, and external calls may release the lock. Free-threaded builds make assumptions about implicit serialization even less appropriate. Prefer workers that return values to one collecting thread, which can aggregate them without a shared mutable counter.

When sharing is necessary, protect the entire invariant with a threading.Lock. For example, checking stock and decrementing it must happen within the same critical section. Keep that section short and avoid holding a lock during slow I/O. A lock cannot fix an unclear ownership model, and multiple locks acquired in inconsistent order can deadlock.

stock.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

04Bound more than the worker count

A worker limit bounds simultaneous execution, but submitting millions of futures can still create a large queue. Feed work in manageable batches or use a producer-consumer design with a bounded queue. Separate per-operation timeouts from a timeout while waiting for a future: stopping the wait does not forcibly terminate a running thread.

Define what happens after partial success. A bulk read may return successful records with explicit failures, whereas a transaction-like operation may need compensation. Do not retry every exception indiscriminately, and never assume cancellation undoes an external effect. Measure throughput, latency, and service limits together before raising concurrency; a larger pool can increase contention without increasing useful work.

LET IT CLICK

The ideas worth keeping.

  • Match thread use to waiting-heavy work and the actual runtime.
  • Read results to observe worker failures.
  • Bound execution, queued work, and external operation duration.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

Make it yours.

Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.

01

Collect failures explicitly

Run square(value) for [2, -1, 3], rejecting negative values. Return sorted successes and the number of failures using submit and as_completed.

your_turn_1.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
from concurrent.futures import ThreadPoolExecutor, as_completeddef square(value):    if value < 0:        raise ValueError('negative value')    return value * valuesuccesses, failures = [], 0with ThreadPoolExecutor(max_workers=2) as pool:    futures = [pool.submit(square, value) for value in [2, -1, 3]]    for future in as_completed(futures):        try:            successes.append(future.result())        except ValueError:            failures += 1assert sorted(successes) == [4, 9]assert failures == 1

Each future is inspected, so worker failures cannot disappear. Sorting makes the acceptance check independent of completion order.

02

Aggregate without a shared counter

Have workers count words in three strings and aggregate their return values in the caller.

your_turn_2.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
from concurrent.futures import ThreadPoolExecutordef word_count(text):    return len(text.split())with ThreadPoolExecutor(max_workers=2) as pool:    total = sum(pool.map(word_count, ['learn Python', 'test it', 'ship']))assert total == 5

Workers do not mutate a common total. For these tiny CPU operations threads add overhead; the exercise demonstrates ownership, not a performance recommendation.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01On a conventional CPython build, which workload commonly benefits from threads?
02A future wait times out. What follows?
03Why prefer returning values to a single collector?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

Collect failures explicitly

Run square(value) for [2, -1, 3], rejecting negative values. Return sorted successes and the number of failures using submit and as_completed.

EXPECTED OUTPUT
[Process exited with code 0 - No output]
challenge.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this chapter, when you want a second angle or the full details.

One chapter closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock chapter completion.