Performance & Beyond Pure Python

Expert · 18 min read · ▶ live playground · ✦ checkpoint

The fastest Python code is not always the cleverest Python code. Often, the win comes from asking: "Am I doing the same work twice, looping in the right place, or using the right runtime?"

Cache work you repeat

Pure Python means code written in normal Python, running through CPython's bytecode interpreter loop. Before you reach for a new runtime or lower-level language, fix the work shape.

Memoization is caching a function's return value so the same inputs can reuse the earlier answer. A cache is stored data kept for reuse. You met functools.lru_cache in Section 12; here it becomes a performance strategy.

This example uses recursion, where a function calls itself with smaller inputs until it reaches a stopping case. Here, ways_to_climb(8) asks for 7 and 6, then those calls keep shrinking until steps <= 2 returns an answer directly.

from functools import lru_cache
 
calls = 0
 
@lru_cache(maxsize=None)
def ways_to_climb(steps):
    global calls
    calls = calls + 1
    if steps <= 2:
        return steps
    return ways_to_climb(steps - 1) + ways_to_climb(steps - 2)
 
 
print("ways:", ways_to_climb(8))
print("real calculations:", calls)
print("cache:", ways_to_climb.cache_info())

ways: 34
real calculations: 8
cache: CacheInfo(hits=5, misses=8, maxsize=None, currsize=8)

Without caching, this staircase-style function keeps asking the same smaller questions again and again. With caching, each steps value is calculated once and reused.

Caching spends memory, and it can return stale answers if the function depends on changing outside state. Use it when the same inputs mean the same answer and recomputation is expensive enough to matter.

Move loops into NumPy

Vectorization means applying an operation across many values through array operations instead of a Python-level loop. You saw this in NumPy: the expression stays short, while the heavy loop runs in optimized code underneath.

This lesson imports NumPy directly. The first time you run the playground, the browser downloads NumPy once for this page; after that, the package stays loaded while your variables still reset on each Run.

import numpy as np
 
minutes = [20, 35, 50, 65]
bonus = [2, 4, 6, 8]
 
python_scores = []
for minute, extra in zip(minutes, bonus):
    python_scores.append(minute * 3 + extra)
 
minutes_array = np.array(minutes)
bonus_array = np.array(bonus)
numpy_scores = minutes_array * 3 + bonus_array
 
print("Python list:", python_scores)
print("NumPy array:", numpy_scores.tolist())
print("high score:", int(numpy_scores.max()))

Python list: [62, 109, 156, 203]
NumPy array: [62, 109, 156, 203]
high score: 203

The two versions produce the same scores. The difference is where the repeated work happens. The Python version walks item by item in your code. The NumPy version hands same-kind array data to compiled numerical code. The old broadcasting analogy still fits: a bonus sticker sheet lines up over a score table.

Try it - choose the loop location

Run this, then change the curve value or add another score. Keep both versions side by side and check that they agree.

python — playgroundlive
⌘/Ctrl + Enter to run

NumPy is not magic dust. For tiny lists, setup can cost more than it saves. The win appears with larger same-kind numerical data and operations NumPy already knows.

Beyond pure Python

When measurement shows pure Python is the bottleneck, you have several exits. They solve different problems.

C extensions are compiled modules written in C or another native language that Python can import. Native code is code compiled for the machine more directly than Python bytecode. Many packages use this shape: friendly Python API outside, fast compiled core inside.

Cython is a tool that lets you write Python-like code with optional type details, then compile it into an extension module. It is useful for a small hot loop when you need tighter control without writing the whole extension by hand.

Pure Python API:
    readable code, fast feedback, easy tests
 
Compiled extension core:
    tight loops, lower-level memory choices, more build complexity
 
Cython:
    Python-like source, optional type details, compiled extension output

The trade is clear. You can gain speed, but you add build steps, platform concerns, and harder debugging. Keep the fast part small and the public Python interface boring.

Different runtime, different trade

An alternative runtime is another implementation that follows Python's language rules but executes code differently. PyPy is an alternative Python runtime with a JIT compiler, a just-in-time compiler that watches running code and compiles hot paths while the program runs.

PyPy can speed up long-running pure Python programs, especially code with many Python-level loops and objects. It may not help code that already spends most time inside NumPy, database drivers, or other compiled extensions. Compatibility with packages also matters.

Do not choose PyPy because a blog post said "faster." Choose it after profiling shows your workload fits: mostly pure Python, long enough for the JIT to notice repeated hot paths, and compatible with your dependencies.

The GIL is changing

The GIL, or Global Interpreter Lock, is CPython's runtime lock that traditionally allows only one thread to execute Python bytecode at a time in a normal GIL-enabled build. You learned the beginner version in concurrency: threads can overlap I/O, but CPU-bound pure Python usually needs processes for parallel CPU work.

Free-threaded Python is a CPython build option that can run without the GIL, allowing multiple threads to execute Python code at the same time. In Python 3.14, the free-threaded build is officially supported but still optional rather than the default. Some extension modules may still need work before they fit this world cleanly.

That direction matters, but the older habits still stand:

  • Measure before changing architecture.
  • Know whether your work is I/O-bound, CPU-bound Python, or already inside compiled code.
  • Protect shared mutable data when threads truly run at the same time.
  • Expect the package ecosystem to matter as much as the interpreter.

The future is not "threads fix everything." Python is gaining a stronger option for CPU parallelism, and your job is still to choose the right tool for the measured bottleneck.

Checkpoint

Answer all three to mark this lesson complete

This closes the under-the-hood arc: you have seen how CPython runs code, how objects expose deep behavior, and where performance work crosses the edge of pure Python. Next section shifts from machinery to judgment: designing code so future-you can still reason about it.

+50 XP on completion