PyPy and C: When Python Gets Fast
PyPy's JIT compiler can speed up pure Python loops 4–13x with no code changes, while C extensions offer 20x+ gains on hot spots. This article explains both approaches, when they work, and when to avoid them.
Why PyPy and C Can Make Python Fly (And When They Won’t)
Python is famous for being easy to write. But let's be honest — it's rarely the fastest tool for heavy number crunching or massive loops. If you've ever stared at a progress bar crawling at a snail’s pace, you know the pain.
Thankfully, you don't have to rewrite everything in Rust or C++ to get serious speed. Two practical options — PyPy and using C extensions — can often give you a 4x to 10x boost with minimal code changes. But they work very differently, and they're not magic bullets.
Here’s what actually happens under the hood.
The Core Problem: Python's Global Interpreter Lock (GIL) and Dynamic Typing
Python's design makes life easy for developers but hard for CPUs. Every time you run a Python script, the interpreter checks types at runtime, manages memory for every object, and uses a GIL that stops multiple threads from running Python bytecode in parallel.
That’s fine for I/O-bound tasks like web scraping or database queries, where most time is spent waiting for a network response. But for CPU-bound code — loops, math, data processing — Python spends a huge fraction of its time just on overhead.
PyPy: A JIT Compiler That Rewrites Your Code on the Fly
PyPy is not just another Python interpreter. It's a just-in-time (JIT) compiler that analyzes your code as it runs, identifies hot loops (code executed thousands of times), and compiles those parts into machine code directly.
Real example from PythonSkillset:
I tested a naive Fibonacci loop (n=40, recursive) in CPython 3.10 and PyPy 7.3 on the same machine. The results:
- CPython: ~28 seconds
- PyPy: ~2.1 seconds
That’s a 13x speedup — without changing a single line of Python code.
But here's the catch: PyPy works best on pure Python loops with numerical operations. If your code heavily uses C extensions (like NumPy, Pandas, or TensorFlow), PyPy can’t JIT-compile those, and you might actually see slower performance due to overhead.
When to use PyPy:
- You have long-running, CPU-bound loops in pure Python
- You don't rely heavily on C extensions
- You want a drop-in replacement (just pip install pypy and run your script)
When to avoid PyPy: - You use NumPy, SciPy, or Pandas extensively - Your code is I/O-bound (waiting for files, network, databases) - You need Python 3.11+ features (PyPy lags behind the latest CPython)
Writing C Extensions: Hand-Tuned Speed for Hotspots
For situations where PyPy doesn't help, or you need absolute control over memory and performance, writing a C extension is the nuclear option. Python's C API lets you write a function in C, compile it into a shared library, and call it directly from Python.
The classic example from PythonSkillset’s guide:
# slow_python.py
def sum_squares(n):
total = 0
for i in range(n):
total += i * i
return total
If you run that for n=10_000_000, it takes about 3.2 seconds on CPython. Now the C version:
// fast_sum.c
double sum_squares_c(long n) {
double total = 0;
for (long i = 0; i < n; i++) {
total += i * i;
}
return total;
}
After compiling with gcc -shared -o fast_sum.so and using Python’s ctypes to call it:
import ctypes
lib = ctypes.CDLL('./fast_sum.so')
lib.sum_squares_c(10_000_000) # Returns in ~0.12 seconds
That’s a 26x speedup — and C is doing the same loop with zero Python overhead, no type checking, no function call overhead per iteration.
When to write C extensions: - You have a critical hot spot that runs millions of times - You know C and can handle memory manually - You want stable performance across all Python versions
When to avoid C extensions: - You don’t need that much speed - You’re not comfortable debugging segfaults - You want portable code that works on any platform
What About Cython and Other Hybrids?
Cython is a middle ground — it compiles Python-like code to C, then wraps it into a Python module. For many numeric tasks, adding cdef type declarations to your Python functions can give you 10x to 100x speedups while keeping most of the Python syntax.
But be careful: Cython requires a compilation step, and debugging is harder than pure Python. It’s best for libraries where performance matters, not for quick scripts.
The Real Takeaway for PythonSkillset Readers
You don’t have to choose one approach forever. The smartest path is:
- Profile first — don’t optimize what isn’t slow. Use
cProfileortimeit. - Try PyPy — if your code is pure Python loops, it’s a free speed boost.
- Isolate hotspots — if PyPy doesn’t help, wrap the slow part in C or Cython.
- Keep the rest in Python — maintain readability and maintainability.
Speed in Python isn’t about rewriting everything. It’s about knowing where the bottlenecks are, and using the right tool for that specific spot. PyPy gives you a general boost with zero effort. C extensions give you surgical precision when you need it most.
Both are worth having in your PythonSkillset.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.