performance Beginner 9 min read

Why Python Is Slow — and What Actually Helps

Bytecode dispatch, boxing and reference counting: where Python's time actually goes, the honest hierarchy of fixes, and what measurement says each one buys.

Published Sep 3, 2026

Python is slow for reasons you can point at in a profiler, not because of a vague trade-off. Every operation an interpreter executes pays three taxes — bytecode dispatch, object boxing, reference counting — and how much each tax costs in your program determines which fix will work and which will waste your week. That ordering matters more than any single technique. Pyvorin Native 1.0.9 addresses exactly one layer of the problem, compilation, and measured across our 71-workload suite it produced a 3.16x geometric mean and a 1.35x median — a real but bounded effect, with 17 workloads running slower. This page explains where the time goes, what removing each cost buys, and the hierarchy of fixes in the order that actually pays.

Where the time goes: three taxes on every operation

Tax one: dispatch. CPython compiles your source to bytecode, then executes that bytecode in a loop: fetch the next instruction, branch to its handler, execute, repeat. A single Python-level addition — a + b — becomes a sequence of bytecodes, each costing a dispatch cycle plus generic operator machinery that must handle every type combination the language allows. In a tight numeric loop, dispatch is the dominant cost, paid on every operation of every iteration, and it is the tax that scales worst: loop ten times longer and you pay it ten times more.

Tax two: boxing. Every value in CPython is a heap object. An integer is an object with a header and a reference count; so is a float. Arithmetic means allocating or recycling these objects and routing through generic operator dispatch. The CPU's fast path — registers, direct arithmetic instructions — sits unused while Python builds scaffolding around each number. Code that does heavy arithmetic on many small values pays this tax constantly.

Tax three: reference counting. Every object knows how many owners it has, and the count is updated on every assignment, argument pass, return value and loop variable. In code that builds and discards millions of small objects — string concatenation in a parser, dicts in a transformation, intermediate lists in a comprehension — the bookkeeping can rival the useful work. Unlike dispatch, this tax follows the objects even into code that calls C libraries, because the objects are still Python objects.

One more cost belongs in the picture even though it is not a tax on operations: algorithmic complexity. An O(n²) join in Python loses to an O(n log n) join in Python by far more than any interpreter overhead ever costs. Complexity dominates constants. It sits at the top of the hierarchy below for exactly that reason.

What removing each cost buys

Each tax has a known antidote, and the antidotes have sharply different price tags and success rates.

The costWhat removes itWhat it buysThe catch
Bad algorithmBetter algorithm or data structureOften orders of magnitude; unbounded upsideRequires understanding the problem, not the tool
Interpreted numeric loopsVectorised libraries (NumPy) or compilationDispatch and boxing removed; measured 10.91x category geomean for numerical computing under compilationRestructuring cost; libraries want array-shaped data
Object churnBatching, fewer intermediates, or compilation with escape analysisReference-count traffic and allocation drop together; object manipulation measured a 42.28x category geomean under compilationOnly where objects genuinely stay local to the function
Time spent in C librariesNothing at the Python level — it is already native coden/aOur parsing category measured 0.86x under compilation; there was no dispatch left to remove

The last row is the one most performance advice skips. When your program's time goes to tokenising text, routing requests or compressing short strings, the interpreter is mostly orchestrating work that already runs at machine speed inside C libraries. Removing "the Python overhead" from that picture removes very little. Our measured suite says so directly: string manipulation at 0.85x, parsing at 0.86x, web request handling at 0.97x — all at or below the speed of the interpreter they started on, because the taxes that dominate those workloads are reference counting on small objects and library-call overhead, not dispatch.

The honest hierarchy of fixes

When a Python program is too slow, there is a correct order of operations. Deviating from it is how teams end up rewriting working code for a tenth of the available gain.

First: algorithmic work. Profile for complexity before anything else. If a hot function is quadratic where it could be linearithmic, no interpreter trick, library swap or compiler will matter next to that fix. This step is free of dependencies and permanent.

Second: vectorised libraries. Where the hot code is numeric, NumPy and pandas move the loop into compiled C without changing the language around it. The data must become array-shaped, which is a real restructuring cost, and the loop must be expressible as vector operations. When both hold, this is usually the best gain-per-effort available.

Third: compilation. Where the hot code is pure Python that libraries cannot express — branching business logic, simulations, non-vectorisable stateful loops — a native-code compiler removes dispatch and boxing directly, on your existing source. This is the layer Pyvorin occupies, and the measured effect is genuine but bounded: a 3.16x suite geomean with a 1.35x median, a best of 202.83x on a trapezoidal integration workload, and a tail of 17 slower workloads where the code shape offered nothing to compile. Compilation is the right third step, not the first or the universal one.

Fourth: hardware. More or faster cores, more memory bandwidth. Real, but linear-ish, paid monthly, and it never fixes an algorithmic problem — it rents a bigger venue for the same quadratic dance. Reach for it when the first three are exhausted and the workload is genuinely compute-hungry.

Notice what the hierarchy implies about measurement: every step after the first assumes you know where the time actually goes. That knowledge comes from a profiler, not from intuition. Reducing Python CPU usage walks the profiling-first workflow in detail; the short version is that cProfile on a production-like run will tell you within minutes whether you are looking at dispatch-heavy loops, library calls, or I/O — and the answer picks the step.

The misdiagnoses that waste the most time

Two wrong diagnoses account for most wasted optimisation effort, and both are visible in a profile if you know to look. The first is blaming the interpreter for library time. When cProfile shows a function such as json.loads or zlib.decompress carrying the runtime, the program is already executing compiled C; a Python-level compiler has nothing to remove there, and the suite's numbers agree — the parsing category, dominated by exactly such work, measured 0.86x. The second is blaming the language for an algorithm. A quadratic deduplication over a growing list will be slow in every implementation of every language; the interpreter's taxes are a constant factor on top, and a small one next to the complexity term. The profiler distinguishes the two in minutes: library rows and a rising call count point one way, your own functions carrying high tottime point the other. Only the second kind is a compilation candidate, and only after the algorithm itself has been sanity-checked.

A third misdiagnosis is subtler: upgrading hardware to treat a software problem. Faster cores raise the constant factor and leave the shape of the profile untouched — the interpreter taxes remain, the library time remains, and the algorithmic problem, if there is one, remains worst of all. Hardware belongs at the bottom of the hierarchy not because it fails, but because it rents a proportional improvement where the earlier steps buy a structural one. Reach for it last, and only once the profile says the remaining time is genuinely compute-hungry loop work that the first three steps have already shaped.

What the measured distribution actually looks like

It is worth putting the full shape of our measured results next to the hierarchy, because the shape is what the third step really buys you. Across 71 workloads, 54 ran faster than CPython and 17 slower; the geometric mean was 3.16x, the median 1.35x, the best 202.83x and the worst 0.05x. Eighteen of the 71 workloads reached at least tenfold — nearly all of them in the pure-loop categories the hierarchy predicts. The median is the number to carry into planning: the typical measured workload completed in about three-quarters of the interpreter's time, and the strong results are real but concentrated. A plan that budgets for the median and treats the top of the table as upside will survive contact with production. A plan budgeted on the best case will not.

Where Pyvorin sits in the hierarchy

Pyvorin is the third step, done honestly. It compiles the functions a profiler flags — locally, in-process, from your existing source — and it is selective rather than universal, which the measurements justify: the categories where dispatch and boxing dominate win strongly (numerical computing 10.91x, core loop kernels 16.24x, object manipulation 42.28x), while the categories where they do not sit at or below parity (string 0.85x, parsing 0.86x, compression 0.19x). A compiler that claimed more than this would be selling the first row of the table and hiding the rest.

Two properties keep compilation honest as a step in the hierarchy. Results are verified against the interpreter's own output before any timing is trusted, so the gain never costs correctness. And where compilation cannot hold, execution diverts to an honest CPython fallback rather than guessing — the semantics of your program are never the price of speed. The fallback path is documented in Unsupported code and the fallback path.

The judgement moment

A team we spoke to had a reporting pipeline that ran for two hours and wanted to know whether a compiler would fix it. The profile answered first: sixty per cent of the time was a quadratic matching loop joining two lists of records, and most of the rest was database I/O. The compiler question was moot until the join became a dict lookup, which took an afternoon and halved the runtime by itself. Only then did the remaining numeric aggregation become worth compiling — and it compiled well, because by then it was isolated, pure-Python and hot. The hierarchy is not dogma; it is the observed order in which these fixes stop being wasted effort.

Where to go next

Last reviewed 3 September 2026 against Pyvorin Native 1.0.9 and the canonical benchmark artefact dated 13 September 2026 (71 workloads: 54 faster, 17 slower; geomean 3.16x, median 1.35x, best 202.83x, worst 0.05x). Category figures are extracted from the artefact, not typed by hand.