Library / SDK
pydata/numexpr avatar
pydata/numexpr

NumExpr's build writes a generated module into the source tree, and its wheel config skips the free-threaded build it documents

Fast numerical array expression evaluator for Python, NumPy, Pandas, PyTables and more

2,543 stars229 forksPythonMIT

At a glance

What is it?
A numerical expression evaluator whose claimed speed-up range starts below parity, whose numpy version appears four times with two different values, and whose documentation warns you not to run the tests in the directory it builds in.
Who is it for?
Read the performance section as the warning it is. The library is engineered for arrays larger than the first-level cache, and it says its own typical gain bottoms out just below parity on a simple expression, so the decision depends entirely on your array sizes.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 15 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 2, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The claimed speed-up range starts below parity and ends at fifteen times

The performance paragraph is the most useful thing on the page and it is honest in a way most project pages are not.

It says common speed-ups relative to the array library are usually between zero point nine five times and four times. Zero point nine five is the floor, and it is attached to the simplest possible expression, adding one to an array. So on a trivial expression the library is very slightly slower than doing the same thing directly.

The four-times figure is attached to a comparison expression combining multiplication, subtraction and a greater-than test, which is the kind of expression that benefits from avoiding intermediate allocations. Above that, the page says higher figures are achievable for some functions and complex operations, up to fifteen times in some cases, without naming which functions.

Then the caveat that matters most: it works best on matrices too large to fit in the first-level CPU cache. Put those three statements together and you have the actual decision rule. The library is tuned for arrays that do not fit in the fastest cache on the machine, and it is near-neutral on arrays that do.

Which means the size of your arrays, not the shape of your expression, is what decides whether installing it helps. On a small frame or a short time series you are adding a dependency for a slight slowdown.

The build writes a generated module into the source tree, and the docs warn you about it

The build script reads a version file at the repository root, and then it writes a module inside the package directory. It opens that file for writing, stamps a header saying it is generated, and writes four values into it: the version string, the version string again under a second name, the version of the array library that was present when the build ran, and the machine architecture the build ran on.

So the package contains a file that is not in the repository, whose contents depend on where you built it, and whose third and fourth lines describe the machine that compiled it rather than the machine that will run it.

And the installation section tells you not to do something about it: do not test the library in the source directory or you will generate import errors. That warning is the visible symptom of the mechanism. Build from a checkout, run the tests, and you have a stale generated module sitting in the import path ahead of anything you meant to test.

This is the old pattern of generating a version module at build time, and it is still common in extension modules. It has one advantage, which is that the version available at runtime matches what was built. Everything else about it is friction, and the file should be in the ignore list and named in the readme.

The readme is honest about the symptom, which is more than most projects manage.

The array library version is declared four times with two different values

Trace one dependency through this repository and you will find it stated four times.

The build requirement asks for version 2 or later of the array library, because building the extension against the older series is not supported by the build tooling. The runtime requirement asks for version 1.26 or later. The requirements file at the root pins the same 1.26 floor and carries a comment telling the reader to keep it in sync with the target array API. And the build script sets a compile-time macro for that API at a version two releases older than 1.26, with a comment saying to keep it in sync with the minimal runtime requirement.

So the two comments promise a synchronisation that the values do not have. The runtime floor and the API the extension is compiled against are different versions, and the comment that says they should match is the reason a reader might believe they do.

code
numpy >= 1.26.0 # keep in sync with NPY_TARGET_VERSION (setup.py)

The compile-against-older-API choice is legitimate and probably deliberate: it is how an extension stays compatible with runtimes whose array library is older than the one you built against. What is not fine is the comment, because a reader auditing the minimum runtime version will read it as an assertion that the macro and the floor agree.

Fixing it is a comment. Ignoring it is a latent bug the next time somebody raises the floor.

The wheel configuration skips the free-threaded build the readme documents

The readme has a section on free-threaded Python, explaining that the global interpreter lock can be disabled from one version onwards and that the library has been demonstrated to work under it. The section recommends either spawning native threads from the main interpreter thread or using Python threads directly, with a warning about oversubscription. The paragraph is cut off mid-word on the page, so the second half of that recommendation is not visible.

Now look at the wheel build configuration. It skips four tags. One is 32-bit x86. One is big-endian PowerPC. One is the IBM mainframe architecture. And one is the free-threaded build of Python 3.13.

So the project documents free-threaded support and publishes no wheel for it. A user on that interpreter installs from source, which means a C compiler, the array library headers for whichever version they have, and on Windows the build tools, in order to use a library whose point is that it saves you work.

The other three skips are unremarkable. Dropping 32-bit x86 and the two big-endian architectures is a reasonable trim for a numerical extension, and a page that lists its supported platforms would be better than one that makes you read the build configuration.

The gap that matters is the one between a documented capability and a shipped artefact, because the section invites exactly the reader who cannot use the wheel.

The fastest path needs a different package manager than the documented one

The readme documents two installation routes and then tells you that one of them is missing the feature you probably want.

The default route is the array library's own wheel index, and the note is explicit: wheels found that way do not include the vector math library support. The alternative route is the conda package manager, and its wheels do include it, when the array library is using that backend.

So the performance feature the page spends a section on is available through one package manager and not the other, in a sentence that sits in the installation section rather than in the section about the feature.

For a source build, enabling it is a configuration step: copy the example configuration file that ships with the distribution to the real filename, edit it to point at the libraries on your system, and watch the build output, because the page tells you to pay attention to the messages to know whether the library was detected. There is then a script in the benchmarks directory that measures the difference and exposes two knobs, one for the accuracy mode and one for the thread count.

That is a well-built path and it is documented as manual. It also means the person who installs with pip and then wonders why transcendental functions are not faster has a documentation answer rather than a bug report, which is the right outcome.

The documentation still tells Windows users about a Python version the package dropped

The manifest requires Python 3.11 or later, and the classifiers list 3.11 through 3.14.

The installation section says, in the middle of its Windows instructions, that for Python 3.6 or later simply installing the latest version of the Microsoft build tools should be sufficient. It also links to a wiki page about Windows compilers that is over a decade old.

So a reader on a current interpreter reads a sentence addressed to an interpreter the package no longer supports, and a link to a page whose advice predates the current build tools. Neither is dangerous and both are exactly what happens when a version floor rises and the prose around it does not.

The rest of that section is better. It warns that a virtual environment on a newer Python than the system one may prompt you to install a compiler, which is the single most common reason a source build of an extension fails on a developer machine. It tells you how to test the installation with a one-line import and a call to the module's own test function. And it points at the requirements file for the required array library version, which is the file that is wrong in the way described above.

The header has its own residue. Six badge definitions are declared and five are used. The unused one is for a continuous integration service that this project stopped using, and it points at that service's domain.

One named maintainer, an institutional address, and a repository organised around a mailing list

The metadata block at the top of the readme is a list of people, and it is shorter than the history of the project.

Two names are given as authors with and others, and that phrase does the work of a long list. Then one person is named as maintainer, with a personal mail address, and the same address is given as the contact. The package manifest, meanwhile, lists the same two names as authors with an institutional domain and names a team rather than a person as maintainer, again with the institutional address.

So the repository presents a single individual and the published package presents an organisation. For a library that a widely used dataframe package depends on internally, that gap is worth noticing: the bus factor stated in the metadata is one person, and the bus factor stated on the page is a team.

The file layout tells the same story from a different angle. There is an announcement file, an author list, release notes, a release process document, and a document listing the functions to add. There is a directory of issues, which in a project like this is a place for reproduction cases, and a benchmarks directory that the readme points you at twice, once for timings and once for the library-specific timing script.

There is also a licensing directory and a license declaration listing both the main file and everything in it, which is the reusable-licensing convention and is more thorough than most projects of this age.

Editorial conclusion

Read the performance section as the warning it is. The library is engineered for arrays larger than the first-level cache, and it says its own typical gain bottoms out just below parity on a simple expression, so the decision depends entirely on your array sizes. Two practical notes before you install. The library declares Python 3.11 and later while its installation text still mentions 3.6, and the free-threaded interpreter it documents has no prebuilt wheel, so that path means building from source.

Frequently asked questions

What is numexpr?

A fast numerical expression evaluator for the array library. It parses expressions into its own op-codes, splits array operands into chunks that fit in the CPU cache, applies the operations chunk by chunk without allocating memory for intermediate results, and distributes those chunks across the available cores. It can also use Intel's vector math library for transcendental functions.

Is numexpr always faster than NumPy?

No. The readme says common speed-ups range from about 0.95 times for a very simple expression up to around 4 times for a more complex one, with higher figures reported for some functions, and that it works best on matrices too large to fit in the first-level CPU cache. On small arrays it adds a dependency for a slight slowdown.

Which Python versions does numexpr support?

The manifest requires 3.11 or later and the classifiers list 3.11 through 3.14, although the installation section still contains an instruction addressed to Python 3.6 and later about the Windows build tools.

Does numexpr publish a free-threaded build?

No. The wheel build configuration skips the free-threaded 3.13 tag, along with 32-bit x86, big-endian PowerPC and the mainframe architecture, so free-threaded users build from source even though the readme has a section on free-threaded support.

How do I get the Intel Math Kernel Library acceleration in numexpr?

Not from the wheels on the array library's own index, which do not include it. The conda wheels do, when the backend is in use. For a source build you copy the example configuration file, edit the library paths, watch the build output for a detection message, and then run the timing script in the benchmarks directory to measure it.

Official sources

  1. License: MIT
  2. Project website
  3. pydata/numexpr on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pydata-numexpr.svg)](https://hysenlabs.com/projects/pydata-numexpr)