The Landscape

Advanced · 16 min read · ▶ live playground · ✦ checkpoint

Slow programs are not all slow for the same reason. Some are thinking hard; others are waiting on a network reply, a file, a database, or a person, and Python's concurrency tools only make sense once you can tell those cases apart.

Two ways to do more than one thing

Concurrency means several tasks are in progress during the same stretch of time. Parallelism means several tasks are executing at the exact same instant, usually on different CPU cores, the processor parts that actually perform program work.

Those sound similar, but they are not interchangeable.

Picture one cook managing several simmering pots. The cook starts rice, chops vegetables while the rice cooks, checks the soup, then comes back to the rice. That is concurrency: overlapping jobs, one active worker.

Now picture three cooks chopping, stirring, and plating at the same instant. That is parallelism: several active workers at once.

This tiny log is concurrent in shape even though nothing here runs in parallel:

events = [
    ("09:00", "Mina", "starts the rice bowl"),
    ("09:01", "Mina", "sets tea water to boil"),
    ("09:02", "rice bowl", "cooks while Mina checks a pickup order"),
    ("09:04", "Mina", "pours tea"),
    ("09:05", "Mina", "finishes the rice bowl"),
]
 
for time, actor, action in events:
    print(f"{time} | {actor}: {action}")

09:00 | Mina: starts the rice bowl
09:01 | Mina: sets tea water to boil
09:02 | rice bowl: cooks while Mina checks a pickup order
09:04 | Mina: pours tea
09:05 | Mina: finishes the rice bowl

The point is not the code. The point is the schedule. Several jobs overlap in clock time because some of them spend time waiting. That is the doorway into concurrency.

Parallelism is a stronger claim. If two CPU cores are actually running two pieces of Python work at the same instant, you have parallelism. If one worker keeps switching attention between waiting tasks, you have concurrency without parallelism.

Waiting-bound vs thinking-bound work

I/O means input/output: reading files, writing files, asking a database for rows, waiting for a web response, or receiving input from a user. An I/O-bound program is limited mostly by that outside waiting.

A CPU-bound program is limited mostly by the processor doing calculations. Image processing, compression, simulations, and big pure-Python loops often live here.

Here is a deliberately rough classifier. The numbers are not seconds; they are just a made-up pressure score so you can see the distinction:

workloads = [
    ("download receipts", 90, 10),
    ("calculate loyalty scores", 15, 85),
    ("read a CSV report", 75, 25),
    ("search every seating plan", 20, 80),
]
 
for name, waiting, computing in workloads:
    if waiting > computing:
        kind = "I/O-bound"
    else:
        kind = "CPU-bound"
 
    print(f"{name}: {kind}")

download receipts: I/O-bound
calculate loyalty scores: CPU-bound
read a CSV report: I/O-bound
search every seating plan: CPU-bound

That label changes your tool choice.

For I/O-bound work, concurrency can help even on one CPU core. If one task is waiting for a file or network response, another task can make progress. That is why web servers, crawlers, chat apps, and automation scripts often care about concurrency before they care about raw CPU speed.

For CPU-bound work, attention-switching is not enough. If the work is truly "do ten billion calculations," you usually need real parallelism, a faster algorithm, or native code that does the heavy lifting outside ordinary Python bytecode.

Threads, processes, and async in one map

A thread is an operating-system-backed line of execution inside one process. Threads share the same memory, which makes communication convenient and shared-state bugs easier to create. You will use them in lesson 17.2.

A process is a separate running program with its own memory. Processes are heavier than threads, but they can run on different CPU cores without sharing the same Python interpreter state. You will use them for true parallelism in lesson 17.2.

Async is cooperative concurrency: tasks voluntarily pause at waiting points so the event loop, a scheduler that runs ready async tasks, can run another task. That model is coming in lesson 17.3, and it is especially strong when you have many I/O-bound tasks.

Start with the pressure, not the tool:

| Pressure | Good first question | Likely direction | | --- | --- | --- | | Many tasks spend time waiting | Can another task use the waiting time? | threads or async | | One calculation burns CPU | Can the work be split across cores? | processes or native libraries | | Tasks change the same list or dictionary | Who owns the shared mutable data? | locks, queues, or redesign | | Thousands of network operations | Can tasks pause without thousands of OS threads? | async |

Shared mutable data means the same changeable object, such as a list or dictionary, is touched by more than one worker. You already know mutation from lists and dictionaries. Add concurrency, and timing becomes part of correctness. Race conditions and locks get their own warning in lesson 17.2.

Try it - classify the pressure

Run this, then add your own task. Do not worry about exact numbers; practice naming whether waiting or computing is the main pressure.

python — playgroundlive
⌘/Ctrl + Enter to run

The GIL, without the fog

CPython is the standard Python implementation most people mean when they say "Python." In the usual CPython build, the Global Interpreter Lock (GIL) is a lock that lets only one thread at a time execute Python bytecode inside one process.

That sentence is narrow on purpose.

The GIL does not mean "Python can do only one thing at a time." It does not mean "threads are useless." It does not mean "Python is always slow." It means CPU-heavy Python code in multiple threads usually will not run Python bytecode on multiple cores at the same instant inside one GIL-enabled process.

There is one modern wrinkle: CPython also has free-threaded builds where the GIL can be disabled. Treat that as a specialized build for now. The mental model above still pays rent because most environments, including browser Python in this course, should be approached as GIL-enabled unless you have deliberately chosen otherwise.

Choose by bottleneck

When a program feels slow, use the measurement-first habit from performance and profiling. Ask what the program is mostly doing:

  • Waiting on outside systems? Concurrency can keep the program responsive and use waiting time.
  • Burning CPU in Python loops? Parallelism needs processes, native libraries, or a better algorithm.
  • Changing shared mutable data? Correctness comes before speed.
  • Serving many independent clients? Concurrency is often the shape of the whole program.

This is why "Should I use threads, processes, or async?" is usually the second question. The first question is: "What is my program waiting for?"

Checkpoint

Answer all three to mark this lesson complete

You now have the map: concurrency overlaps waiting, parallelism runs work at the same instant, and the GIL shapes thread behavior in normal CPython. Next, you will use the operating system's two blunt instruments directly: threads and processes.

+50 XP on completion