Numba and JIT Compilation
Numba compiles ordinary Python functions into machine code the first time they run, turning slow number-crunching loops into speeds close to hand-written C — without leaving Python.
Prerequisites: NumPy Arrays and Dtypes
Python loops are slow because every iteration re-does work a compiled language would have already figured out once: checking each variable's type, looking up which version of + applies, allocating and deallocating small objects. A million-iteration Monte Carlo loop written in plain Python can take seconds where equivalent C code takes milliseconds — not because Python's arithmetic is wrong, but because it re-derives "how do I add these two things" a million separate times.
Numba is a just-in-time (JIT) compiler for Python that fixes this without requiring you to write C. Decorate a function with @numba.jit, and the first time it's called, Numba inspects the actual types of its arguments, compiles a specialized machine-code version for exactly those types, and caches it. Every subsequent call with the same argument types skips Python entirely and runs the compiled version directly.
Numba trades a one-time compilation cost, paid on the first call, for near-C speed on every call after that. It works by compiling to concrete types it discovers at runtime, so it shines on numeric loops with fixed, simple types and struggles with the flexible, dynamically-typed Python that doesn't map cleanly onto machine instructions.
Why plain Python loops are slow
Every Python integer is actually a full object carrying type information, a reference count, and its value — nothing like a raw machine integer. A loop that adds two Python floats a million times pays object-overhead costs a million times. Numba's compiled version, once specialized to float64, operates on raw machine floats sitting in CPU registers, exactly like C would.
Worked example
A rolling volatility calculation over 2 million price points, written as a plain Python for loop, takes roughly 1.8 seconds. Adding @numba.njit above the function and re-running it: the first call takes about 0.3 seconds (compilation happens here), and every call after that takes about 0.02 seconds — a roughly 90x speedup over the interpreted version once the compiled cache is warm. The speedup comes entirely from skipping per-element type checks and dynamic dispatch; the arithmetic itself hasn't changed.
A second case shows the failure mode: the same function, but one branch appends to a Python list of mixed types (some int, some str) depending on a condition. Numba cannot compile this in nopython mode, because it cannot commit to one concrete type for that list — it either falls back to slow "object mode" or raises a compilation error outright, defeating the purpose entirely.
What this means in practice
Numba is worth reaching for on tight numerical loops that NumPy's own vectorized functions can't express cleanly — custom recursive filters, path-dependent Monte Carlo payoffs, or anything with data-dependent branching inside a hot loop. It is not a general Python accelerator: string processing, dictionaries with mixed value types, and most object-oriented code either compile poorly or not at all.
The first call to a JIT-compiled function pays the full compilation cost, which can be slower than just running it in plain Python once. Benchmarking a Numba function by timing a single call and concluding "it's not faster" is a common mistake — always warm up the function with one throwaway call before timing it, exactly as a live system would after startup.
Related concepts
Practice in interviews
Further reading
- Numba documentation, 'A ~5 minute guide to Numba'