rust-numpy: NumPy arrays in Rust without the raw pointers
PyO3-based Rust bindings of the NumPy C-API
At a glance
- What is it?
- rust-numpy is a crate that exposes the NumPy C API to Rust through PyO3, so an extension module can accept and return arrays as typed Rust values rather than as raw buffers, and it also works in the other direction, letting a Rust program run Python code and pull the result back as an array. It suits anyone writing numeric code that has to live on both sides of the Python boundary.
- Who is it for?
- Adopt rust-numpy if you are writing a Python extension in Rust or a Rust program that has to call into Python, because it gives you NumPy arrays as Rust values in both directions and handles the conversion for you. Do not adopt it if your array handling stays on one side of the boundary, since a plain Python extension is then less machinery.
- Can I use it commercially?
- Yes. BSD-2-Clause 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 2 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the crate actually is
rust-numpy is a set of Rust bindings for the NumPy C API, built on PyO3, and it is published on crates.io under the short name numpy, with the documentation on docs.rs.
The value proposition is about the boundary. The NumPy C API is a C interface, and calling it from Rust means dealing with raw pointers, with strides, with the dtype system and with the rules about when the GIL must be held. This crate sits on top of that and gives you array types that behave like Rust values, so a function that takes an array takes it as a typed view with a lifetime, and a function that returns an array returns a Rust owned buffer that converts to a NumPy array when you hand it to Python.
The requirements section is short and concrete. Rust 1.85.0 or later, and the note explains that the minimum supported Rust version basically follows the one of PyO3, which is the sensible arrangement for a crate that is a binding layer. Python 3.9 or later, and the changelog of that requirement is recorded in the README, with Python 3.7 support dropped from version 0.29 and 3.8 support dropped from 0.30. And NumPy itself has to be installed in your Python environment, with 1.16.0 recommended although older versions may work.
The other Rust libraries required are ndarray, which is the Rust side matrix library, and PyO3 itself, plus a few more listed in the manifest.
The crate is BSD-2-Clause licensed, the current release is 0.30.0 from 2026-10-15, and the last push to the repository was on the same day.
Direction one: writing a Python extension in Rust
The first example is the one most people come for, a Python module implemented in Rust, and the example is a small numerical routine that shows both access patterns.
The manifest for such a project is short, because the crate type is what makes it a Python module.
[lib]
name = "rust_ext"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.30" }
numpy = "0.30"The cdylib crate type is the part to remember, and forgetting it is the usual reason a first attempt does not import.
The code defines two functions. The first takes immutable views of two arrays and returns a new one, computing a scaled sum with the arithmetic written against Rust array types rather than against raw memory.
// example using immutable borrows producing a new array
fn axpy(a: f64, x: ArrayViewD<'_, f64>, y: ArrayViewD<'_, f64>) -> ArrayD<f64> {
a * &x + &y
}
// example using a mutable borrow to modify an array in-place
fn mult(a: f64, mut x: ArrayViewMutD<'_, f64>) {
x *= a;
}The distinction between the two is the one that matters in practice. A read-only view borrows the Python array's memory without copying it, and a mutable view writes into it in place, which is the whole reason to use NumPy from a systems language rather than copying data across the boundary.
The wrappers then translate between the Python types and the Rust ones, with the PyO3 function attribute and the conversion traits for arrays, and the example is complete enough to copy.
Direction two: Rust running Python
The second example runs the opposite way, a Rust program that starts Python, evaluates an expression and gets a typed array back.
The dependency needs one extra feature.
[package]
name = "numpy-test"
[dependencies]
pyo3 = { version = "0.30", features = ["auto-initialize"] }
numpy = "0.30"The auto-initialize feature is what lets a plain binary embed the interpreter without a separate initialisation step, and the difference from the extension case is that here there is no Python to initialise you.
The code attaches to the interpreter, imports numpy as a module, evaluates a NumPy expression with the locals dictionary, and casts the result into an array type.
let pyarray = py
.eval(c"np.absolute(np.array([-1, -2, -3], dtype='int32'))", Some(&locals), None)?
.cast_into::<PyArray1<i32>>()?;
let readonly = pyarray.readonly();
let slice = readonly.as_slice()?;
assert_eq!(slice, &[1, 2, 3]);Two things are worth noticing. The cast is a checked conversion, so a result that is not an array of the expected type is an error rather than a wrong answer. And the read-only view is turned into a plain slice, which is how you get at the data without copying it.
For a project that needs to call an existing Python library from Rust, this is the path, and it avoids writing a binding for every function you use.
The ndarray version trap, and the fix
There is a section on the dependency on ndarray that every user should read, because it describes a Cargo behaviour that surprises people.
The crate uses types from ndarray in its public API, and ndarray is re-exported in the crate root so that you do not need to specify it as a direct dependency. That is a kindness, because otherwise every user would have to keep their ndarray version in step with the crate.
But the crate is compatible with multiple versions of ndarray and therefore depends on a range of semver incompatible versions, currently from 0.15 up to and including 0.17. And Cargo does not automatically choose a single version of ndarray if you depend directly or indirectly on anything outside that exact range.
The README gives the concrete case. If you specify numpy 0.30 and ndarray 0.15 in your own manifest, the build will depend on both 0.15.6 and 0.16.1 of ndarray by default, even though 0.15.6 is inside the supported range. The fix is to pin it yourself.
cargo update --package ndarray:0.16.1 --precise 0.15.6The consequence of not doing this is two copies of ndarray in the build, which means the types do not match across the boundary and the compiler complains in a way that does not obviously point at the cause. It is one of those problems that costs an hour once and five minutes every time after it, because the fix is a single command and the reason is written down here.
The manifest, the benches and the lint policy
The Cargo manifest of the crate itself is a good place to see how the project is built, and a few entries are worth noting.
The optional dependencies are the interesting ones. There is a half precision type, a linear algebra crate with a wide version range, and a random number core, all optional, which means the crate has features for the numerical work that is not needed for every user. The build configuration dependency is the PyO3 build config crate with the resolve-config feature, and that is what lets the build find the Python interpreter and the NumPy headers on the build machine.
The benchmark section declares three benchmarks, named array, array_like and borrow, and each one sets the harness to false because they are written against the criterion benchmarking library rather than the built in test harness. The borrow benchmark is the interesting one, since it measures the thing the crate exists for, which is passing an array across the boundary without copying it.
The lint configuration is short and deliberate. The Rust lints deny elided lifetimes in paths, with the comment that the GIL lifetime is usually wanted to be explicit. And the clippy lints allow needless lifetimes, which is the counterpart, since the explicit lifetimes the first rule demands would otherwise trip the other one. Two rules that contradict each other in the general case, resolved by turning one off, is a reasonable outcome and a good example of a project that has thought about its own lint budget.
The repository layout has the standard pieces, with a build script, the source, the tests, the benches and the examples, and an x.py script, which is the PyO3 project's own build helper.
The examples, and the alternative to a binding layer
The examples directory has three projects and they cover the three ways you might use the crate.
The simple one is how to get started, and it is the reference for a first extension module. The linalg one uses the ndarray-linalg crate, so it shows the crate working with a heavier numerical stack where the array types are produced by another library rather than by NumPy itself. The parallel one uses rayon, so it shows what happens when the work inside the function is threaded.
That third example is worth a moment, because it addresses the question everyone has about Python extension modules. The GIL is the reason Python extensions are often written in C, and the reason people reach for a systems language. A Rust extension can release the GIL while it works, so the numeric part runs in parallel and the interpreter is not blocked, and the rayon example is the demonstration that the machinery supports that.
The alternative to a binding layer is to use the NumPy C API directly from Rust, without this crate. The difference is entirely in what you work with. The C API gives you pointers, strides, a dtype enumeration and a set of rules about reference counting, and you own all of it. This crate gives you the same data as Rust values with lifetimes, with the conversions done for you, and the cost is a dependency on ndarray and on the version range that comes with it.
So the choice is between control and convenience. If your code is a tight numerical kernel called with large arrays, the conversions matter and the crate is worth the dependency. If you are wrapping a handful of functions for a Python caller, the plain C API is more code but nothing to keep in step.
Editorial conclusion
Adopt rust-numpy if you are writing a Python extension in Rust or a Rust program that has to call into Python, because it gives you NumPy arrays as Rust values in both directions and handles the conversion for you. Do not adopt it if your array handling stays on one side of the boundary, since a plain Python extension is then less machinery. Before you add it to a project, check the ndarray version your dependency tree already resolves, because the crate supports a range of ndarray versions and Cargo will not unify them for you.
Frequently asked questions
What is rust-numpy?
It is a set of PyO3 based Rust bindings for the NumPy C API, published on crates.io as numpy. It gives Rust code typed array views and owned arrays that convert to and from Python NumPy arrays, in both directions, so an extension module can work on array data without touching raw pointers.
What are the version requirements for rust-numpy?
Rust 1.85.0 or later, with the minimum Rust version following the one of PyO3, Python 3.9 or later, with 3.7 support dropped from 0.29 and 3.8 support dropped from 0.30, and NumPy installed in your Python environment, with 1.16.0 recommended although older versions may work.
Why does my build end up with two versions of ndarray?
The crate uses ndarray types in its public API and is compatible with a range of semver incompatible ndarray versions, currently 0.15 to 0.17, and Cargo does not automatically choose a single version if anything in your dependency tree falls outside that exact range. The fix is to run cargo update --package ndarray:0.16.1 --precise 0.15.6 to unify them yourself.
Can a Rust program call into Python and get a NumPy array back?
Yes. With the auto-initialize feature of PyO3 enabled, a Rust binary can attach to the interpreter, import numpy, evaluate an expression and cast the result into a typed array, and then read it through a read-only view as a plain slice.
Official sources
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.
[](https://hysenlabs.com/projects/pyo3-rust-numpy)Was this useful?