Watch the video, transcript and sources
You think Python needs a new language to get fast. The same CPU ran our batch faster after the loop moved. Where did the work go? When does the speedup vanish? Which shortcut brings Python back? CPython is the standard Python implementation. It compiles your source into bytecode, then runs those instructions in an interpreter. NumPy is a numerical library whose arrays hold values with a declared data type. Its built in arithmetic can hand a whole batch to compiled operations.
The workload and its limits
The expression is x*x + x, with the same numerical values in every implementation. The ready-input case gives each function its usable representation; its outputs can be a list or an array. The second case requires a Python list at both ends and includes the array conversions. Both scopes include result allocation and release.
These are measurements of one expression on CPython 3.10.12, NumPy 2.2.6 and warmed Numba 0.67.0, on an i9-13900HX under WSL2 with one logical CPU selected. Eleven repeated trials produced each median. Forty exact output comparisons passed. Shared-system activity and uncontrolled CPU frequency can influence the timings; the table is neither a universal language ranking nor a latest-Python claim. No GPU was benchmarked, and the test did not measure the expression's SIMD instruction mix, multicore scaling or peak memory.
The initial Numba compile and execution took 0.963 seconds and was excluded from the warmed per-call medians. At eight ready values, comprehension took 0.314 microseconds and NumPy took 0.510. Setup and boundary costs change the answer.
Follow the expensive loop
Take a million floating point values, square each one, then add the original value. Every version gets identical values and produces matching answers. The values are small enough to keep rounding differences out of the race. On this machine, the comprehension took about thirty three milliseconds. NumPy took less than one. Both ran on the CPU. That's useful evidence for this workload, and a terrible obituary for a programming language. Start with the ordinary loop. Python gets the next value, performs the arithmetic, and appends the answer. Then it does that again. The multiplication is cheap. Arranging the next multiplication, and handling the Python objects around it, adds work every time.
This is the tested function's actual disassembly. Iteration, multiplication and addition appear as bytecode instructions. Python versions and interpreters make different choices. These timings belong to the version on screen, which is Python three point ten. A Python list stores references to objects, so different kinds of things can share a list. Our arithmetic job uses numbers throughout. It pays for a general arrangement while requesting a repetitive service. Imagine ordering the same coffee a million times and filling out a fresh form for every cup. The coffee machine is fine. The paperwork has developed its own career.
NumPy changes the contract. This array holds fixed size floating point values with one declared type. Our array is contiguous, so its values sit together in memory. Views can have other layouts. The arithmetic operation knows what kind of data it is receiving. The call hands the batch to a native operation. A loop still visits the values inside compiled code. It can reuse knowledge of the type across that work. Python remains the person handing over the job. Itamar Turner Trauring explains the distinction in Python Speed. Batch interfaces, native implementations and processor vector instructions are separate things. The function name supplies very little evidence.
Include what the caller needs
This result used NumPy on the CPU. A GPU backend such as CuPy chooses a different home for arrays and operations. Buying a GPU leaves this ordinary Python loop running where it already ran. CPU vector instructions can process several values together. NumPy has machinery for choosing supported kernels. Our runtime reports available features, but doesn't trace this expression's instructions. Giving the entire measured gain to vector instructions would oversell the evidence. The GPU shopping cart can stay closed. There is something deeply satisfying about a performance fix that also cancels a purchase. Unfortunately, the benchmark continues.
With eight values, the comprehension won. Calling the array machinery cost more than the tiny job. Larger ready arrays pulled ahead. We used repeated trials and a warmed process. Newer Python builds can change the crossover. Then change what the application needs. Begin with a Python list and finish with a fresh Python list. NumPy must convert the input into an array and turn the answer back into Python objects. Those steps belong on the bill if your application needs them. At a million values, that complete path took about twenty eight milliseconds. The comprehension took about twenty eight too. Our dramatic lead became a near tie. The fast part was real. The advantage depended on letting the data stay in its numerical form.
Follow the data. If the next operation accepts an array, keep the array. If you repeatedly convert between lists and arrays, measure those crossings. Stopping before an expensive handoff gives accurate numbers for the wrong question. Multiplying this array creates a temporary result. Adding the original values creates the output. Native execution still needs memory and moves data. Some operations accept an output buffer, reducing allocations when your code can safely reuse it. One pretty line can order several large buffers. Python Speed calls out this problem too. The allocator is remarkably indifferent to your aesthetic.
You can also keep loop syntax and change its execution. Numba can compile supported numerical Python functions into native code. Here is the loop from our test with its compiler decorator. It still says for. The expensive interpreter path has changed. After compilation, this loop took less than half a millisecond with ready arrays. Compilation itself took about a second in this run. That setup cost was excluded from the repeated timings, so a tiny one off job deserves a different comparison. Numba's own guide is happy to show explicit loops. Starting a language migration before finding the hot path is an elaborate way to avoid opening a profiler.
The vectorize callback trap
First find what consumes the time. Network waits, object parsing and bulk arithmetic call for different fixes. Native array operations help when the calculation and data fit. Measure the result your caller actually needs. Now for that shortcut. You have a Python function and want it to accept an array. A wrapper named vectorize sounds like the missing performance button. Its documentation has an important sentence waiting. NumPy says its vectorize wrapper is provided primarily for convenience. Its implementation is essentially a for loop. The wrapper accepts batches, but keeps calling your Python function for successive elements. The interface got shorter. The execution boundary stayed busy.
Watch the same values come back through the Python callback. With ready arrays, this version took about eighty seven milliseconds. It was slower than our comprehension. A useful broadcasting wrapper can preserve the exact per element work you were trying to remove. Check the implementation behind a batch interface, keep compatible data in its useful form, and include conversion in the comparison. Native, callback and compiled loops can look similar from the caller's chair. The name vectorize made a promise your stopwatch never signed. Keep it when convenience is what you need. For performance, check the loop that runs.
My verdict is ship it for batching a measured numerical hot path. Keep ordinary loops when they already work well. Check where the work happens and what the next caller needs.
Verdict: SHIP IT — Batch the measured numeric hot path
Sources
And that's the diff for today. I'm Niko from Axrisi. Merge responsibly.
YouTube · Newsletter · thedailydiff.dev · forward this to the intern who deployed on Friday.

