CPython vs. PyPy: Which Python runtime is faster?

12 Min Read

Ever wondered how CPython’s shiny new native-JIT compiler measures up against PyPy? We put them head-to-head in a series of benchmarks, and the results might just throw you for a loop!

stop watch on a racetrack

PyPy, that alternative Python runtime, has always been known for its incredible speed. It uses its own special JIT compiler to deliver some seriously huge performance boosts compared to CPython, which is the standard Python you’re probably used to.

However, PyPy’s fantastic speed often comes with a trade-off: compatibility. It can sometimes struggle to play nicely with the broader Python ecosystem, especially C extensions. While things are getting better, PyPy itself can be a bit behind when it comes to supporting the very newest Python versions.

On the other side, recent CPython releases have started to include their own native JIT compiler. The idea is to eventually get much better performance, and we’re already seeing some good speed-ups in certain tasks. Plus, CPython now offers a special build that removes the Global Interpreter Lock (GIL), opening the door to truly free-threaded operations and even more potential speed gains.

So, the big question is: could CPython really catch up to, or even surpass, PyPy in terms of raw speed? To find out, we put PyPy and the latest CPython builds (including those with JIT and the no-GIL feature) through identical benchmark tests. The findings were pretty interesting!

PyPy Remains the King of Raw Math

CPython has traditionally struggled with simple math calculations. That’s because it involves a lot of behind-the-scenes steps and layers of abstraction; for example, it doesn’t have a direct, machine-level way to handle integers like some other languages.

Because of this, you’ll often see CPython perform quite slowly on benchmarks like the one below:

 

def transform(n: int):
    q = 0
    for x in range(0, n * 500):
        q += x
    return q

def main(): return [transform(x) for x in range(1000)]

main()

Running this benchmark on a six-core Ryzen 5 3600, Python 3.14 took roughly 9 seconds. PyPy, however, absolutely devoured it in a blink, finishing in about 0.2 seconds!

Interestingly, this particular task doesn’t seem to get much help from Python’s JIT, at least for now. Even with the JIT turned on in 3.14, the time only barely dropped to about 8 seconds.

But what if we tried a multi-threaded version of this same code, and then threw Python’s special no-GIL version into the mix?


def transform(n: int):
    q = 0
    for x in range(0, n * 500):
        q += x
    return q

def main(): result = [] with ThreadPoolExecutor() as pool: for x in range(1000): result.append(pool.submit(transform, x)) return [.result() for in result]

main()

The change is pretty wild, to say the least! Python 3.14 managed to finish this task in 1.7 seconds. While that’s still not as lightning-fast as PyPy’s sub-second times, it’s a huge improvement, making the combination of threads and no-GIL definitely worthwhile.

And PyPy with threading? Surprisingly, running the multi-threaded version on PyPy *slows things down considerably*, taking about 2.1 seconds to complete. This is because PyPy still uses a GIL-like locking mechanism, meaning it can’t achieve true parallelism across threads. For PyPy, its JIT really shines when everything runs in a single thread.

You might be thinking, ‘What about using a process pool instead of a thread pool?’ Well, not really. While a process pool version *does* offer a slight speed improvement on PyPy (around 1.3 seconds), it’s important to remember that PyPy’s process pools and multiprocessing aren’t as finely tuned as CPython’s.

 

So, let’s quickly summarize for plain “vanilla” Python 3.14:

  • Without JIT and with the GIL: 9 seconds
  • With JIT but still with the GIL: a slightly better 8 seconds
  • Without JIT but using the no-GIL build: 9.5 seconds

It’s worth noting that the no-GIL build is a tad slower for single-threaded tasks compared to the regular build. The JIT offers a small boost here, but nothing major.

Now, let’s look at how Python 3.14 performs when using a process pool:

  • Without JIT and with GIL: 1.75 seconds
  • With JIT and GIL: 1.5 seconds
  • Without JIT but with no-GIL: 2 seconds

What about other variations of the script using Python 3.14?

  • Threaded version with no-GIL: 1.7 seconds (a significant improvement!)
  • Multiprocessing version with GIL: 2.3 seconds
  • Multiprocessing version with GIL and JIT: 2.4 seconds
  • Multiprocessing version with no-GIL: 2.1 seconds

And here’s the rundown for PyPy:

  • Single-threaded script: a blistering 0.2 seconds
  • Multi-threaded script: 2.1 seconds
  • Multiprocessing script: 1.3 seconds

Tackling the N-Body Problem

The “n-body” benchmark is another classic math challenge where standard Python tends to struggle. It’s also a problem that’s tricky to accelerate with parallel computing—while possible, it’s far from simple, so most easy implementations stick to a single thread.

After running the n-body benchmark for a million repetitions, here are the numbers we saw:

  • Python 3.14 (no JIT): 7.1 seconds
  • Python 3.14 (with JIT): 5.7 seconds
  • Python 3.15a4 (no JIT): 7.6 seconds
  • Python 3.15a4 (with JIT): a solid 4.2 seconds

That’s a pretty strong performance for the JIT-enabled Python versions. However, PyPy absolutely *flies* through the exact same benchmark in a mere 0.7 seconds—right out of the box!

Calculating Pi

It might surprise you, but even PyPy can hit a snag with some math-intensive Python programs. Take this straightforward script for calculating pi’s digits, for example. This kind of job isn’t really suited for heavy parallelization, so we’re testing it as a single-threaded process.

Here’s what happened when we ran it to compute 20,000 digits:

 
  • Python 3.14 (no JIT): 13.6 seconds
  • Python 3.14 (with JIT): 13.5 seconds
  • Python 3.15 (no JIT): 13.7 seconds
  • Python 3.15 (with JIT): 13.5 seconds
  • PyPy: a surprisingly slow 19.1 seconds

While not common, it’s certainly not unheard of for PyPy to perform worse than standard Python. What really catches you off guard here is seeing it happen in a situation where you’d typically expect PyPy to truly shine.

CPython is Stepping Up Its Game for Other Tasks

I often use another benchmark that’s based on the Google n-gram analysis. It involves crunching a multi-megabyte CSV file to pull out some statistics. This particular task is more about input/output (I/O) than pure CPU power, unlike our previous tests, but it still gives us valuable insights into how fast these runtimes truly are.

Let’s see how Python 3.14 tackles this benchmark using various script configurations:


import collections
import time
import gc
import sys
try:
    print ("JIT enabled:", sys._jit.is_enabled())
except Exception:
    ...

def main(): line: str fields: list[str] sum_by_key: dict = {}

start = time.time()

with open("ngrams.tsv", encoding="utf-8", buffering=2 << 24) as file:
    for line in file:
        try:
            fields = line.split("\t", 3)
        except:
            continue
        try:
            sum_by_key[fields[1]] += int(fields[2])
        except:
            sum_by_key[fields[1]] = int(fields[2])

summation = collections.Counter(sum_by_key)
max_entry = summation.most_common(1)
stop = time.time()
print(stop - start)

if len(max_entry) == 0:
    print("No entries")
else:
    print("max_key:", max_entry[0][0], "sum:", max_entry[0][1])

try: gc.freeze() gc.disable() except Exception: … main()

Here’s how Python 3.14 handles this benchmark with different versions of the script:

  • Single-threaded, with GIL: 4.2 seconds
  • Single-threaded, with JIT and GIL: 3.7 seconds
  • Multi-threaded, with no-GIL: a blazing 1.05 seconds
  • Multi-processing, with GIL: 2.42 seconds
  • Multi-processing, with JIT and GIL: 2.4 seconds
  • Multi-processing, with no-GIL: 2.1 seconds

And now, let’s look at the same scenario with PyPy:

  • Single-threaded: 2.75 seconds
  • Multi-threaded: a whopping 14.3 seconds (and nope, that’s not a typo!)
  • Multi-processing: 8.7 seconds

To put it plainly, in this specific test, CPython’s no-GIL multi-threaded setup actually outperformed PyPy even at its best! Currently, there isn’t a CPython build that combines both JIT and free threading, but one is on the horizon, and it could definitely shake things up even more.

The Bottom Line

To wrap things up, when it comes to raw, unoptimized math-heavy scripts, PyPy still generally beats CPython hands down. However, CPython sees huge gains when it can leverage free-threading and even multiprocessing, making it much more competitive in those situations.

PyPy, unfortunately, can’t fully utilize these built-in parallel features. But honestly, its core speed is often so impressive that you might not even *need* threading or multiprocessing for many tasks. Think about problems like the n-body simulation, which is tough to parallelize effectively, or calculating pi, which barely allows for parallelization at all—here, PyPy’s single-threaded speed is a real blessing.

The biggest takeaway from all these tests is that PyPy’s advantages aren’t a one-size-fits-all solution; they’re not even consistent. The benefits swing wildly based on the exact situation, and even within a single program, you can find a huge range of performance. Some applications can achieve mind-blowing speeds with PyPy, but predicting which ones will isn’t straightforward. The only real way to know for sure is to benchmark your own code.

It’s also crucial to remember that a key path to better overall Python performance and true parallelism—free-threading—isn’t something PyPy currently offers. Even multiprocessing doesn’t perform optimally with PyPy, largely because its method for serializing data between processes is significantly slower than CPython’s.

 

No matter how quick PyPy can be, these benchmarks really highlight the power of genuine parallelism with threads in specific situations. While PyPy’s developers might eventually figure out a way to integrate this, it’s improbable they could simply adopt CPython’s approach, given the deep architectural differences between the two.

 
PythonProgramming LanguagesSoftware Development
Share This Article
Leave a Comment

Leave a Reply

Your email address will not be published. Required fields are marked *