Python did not win on speed. It won because the distance between an idea and a working program is shorter in Python than almost anywhere else, and because that shortness compounded into the largest scientific and machine learning ecosystem in software. Understanding how CPython executes your code explains both the appeal and the limits.
From source to bytecode
CPython compiles your source to bytecode — a compact instruction set for a stack machine — and caches it in __pycache__. There is no machine code involved (until 3.13's experimental JIT); a C loop called the evaluation loop reads instructions one at a time and executes them. You can look at the result directly, which is one of the best ways to build intuition about cost.
import dis
def total(items):
return sum(x.price for x in items)
dis.dis(total)
# LOAD_GLOBAL sum
# LOAD_CONST <genexpr>
# MAKE_FUNCTION
# ...
# CALL 1
# RETURN_VALUEEverything is an object, including integers, and every operation goes through the object protocol. Adding two numbers means unboxing two heap objects, dispatching through a type's method table, allocating a result. That indirection is the source of Python's slowness and also of its flexibility — the same machinery is what lets NumPy overload arithmetic to operate on entire arrays.
Memory: reference counting plus a cycle collector
Every object carries a refcount, incremented and decremented as references appear and vanish; at zero, it is freed immediately. That gives predictable, prompt cleanup — a file closes the moment its last reference goes away — but cannot handle cycles, so a generational garbage collector periodically sweeps for unreachable groups. Practical consequences: deleting a large structure frees memory right away, __del__ ordering is not something to depend on, and gc.freeze() before forking keeps copy-on-write pages shared in pre-fork servers like Gunicorn.
The GIL, precisely
The Global Interpreter Lock ensures exactly one thread executes Python bytecode at a time. It exists because refcounting is not thread-safe, and making every refcount atomic would slow single-threaded code substantially. The GIL is released around blocking I/O and inside C extensions that opt out of it — which is why threads are perfectly good for network calls and why NumPy, Pandas, and PyTorch achieve real parallelism despite it. The GIL only blocks parallel execution of pure-Python CPU work.
- I/O-bound work → threads or asyncio. The GIL is not in your way.
- CPU-bound pure Python → multiprocessing, or push the hot loop into NumPy, Cython, Numba, or Rust via PyO3.
- Python 3.13+ ships an optional free-threaded build (PEP 703) with no GIL. It is real, it is improving, and it is not yet the default — treat it as a horizon, not a plan.
asyncio in one paragraph
async def creates a coroutine; awaiting it suspends the function and hands control back to an event loop that resumes it when the awaited operation is ready. It is cooperative concurrency in a single thread — one blocking call in an async handler stalls every other task in the process. The rule is simple and unforgiving: never call blocking code inside async code without wrapping it in a thread executor.
import asyncio, httpx
async def fetch_all(urls: list[str]) -> list[str]:
async with httpx.AsyncClient(timeout=5.0) as client:
tasks = [client.get(u) for u in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
return [r.text for r in results if not isinstance(r, Exception)]Why it is still the right choice, often
For data work, machine learning, automation, scripting, and a very large share of web backends, the bottleneck is a database, a network, or a GPU — not the interpreter. Python's real speed is the speed of the team using it: readable code, an enormous standard library, and packages for essentially everything. Add type hints and mypy or pyright, and you get much of the safety people leave Python for, without leaving Python.
Where it stops being the right tool: latency-critical hot paths measured in microseconds, memory-constrained environments, heavy CPU-bound parallelism, and anything that must ship as a small self-contained binary. The mature pattern is not to switch languages but to draw the border deliberately — Python for orchestration, glue, and iteration speed; a compiled language for the twenty lines that actually need to be fast.