PyO3/maturin: building and publishing Rust-backed Python packages
Build and publish crates with pyo3, cffi and uniffi bindings as well as rust binaries as python packages
At a glance
- What is it?
- maturin is a Rust build tool that turns pyo3, cffi and uniffi crates, plus plain Rust binaries, into installable Python wheels. Its value is that it removes the hand-written packaging glue, and its cost is that wheel compatibility still has to be handled deliberately.
- Who is it for?
- Adopt maturin if you already have a Rust crate and want pip-installable wheels without maintaining setuptools-rust glue, especially if you only ever ship CPython targets. Skip it if you need a pure-Python wheel, or if you cannot run the manylinux container, Zig or maturin-action in CI for Linux artifacts.
- Can I use it commercially?
- Yes. Apache-2.0 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 3 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 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The packaging gap maturin closes for Rust extensions
A Rust extension module is not a Python package. Before it can be installed with pip, someone has to compile the crate for a specific interpreter and platform, rename the resulting shared object into the importable module name, copy any Python sources alongside it, write the metadata that pip reads, and zip the whole thing into a wheel with the correct platform tag. Historically that work was done with setuptools-rust and a setup.py, which means a second build system layered on top of Cargo, with its own failure modes when the two disagree about target directories or feature flags.
maturin targets that gap directly. The README describes it as building crates with pyo3, cffi and uniffi bindings, as well as Rust binaries, as Python packages with minimal configuration. The audience is Rust developers who want their crate to be pip installable, and Python teams who want a native component shipped through the normal PyPI channel rather than through a separate binary distribution. The README notes that maturin needs no extra configuration files and does not clash with an existing setuptools-rust configuration, so it can be introduced into a repository that already has one.
It is not a binding generator. pyo3, cffi and uniffi each define how the Rust and Python sides talk to each other; maturin packages the result. If you have no Rust crate yet, maturin new will scaffold one, but the interesting work still happens in the binding layer.
How maturin decides what to build and what to name it
The build is driven by two manifests. Cargo.toml supplies the crate, its version and its targets; pyproject.toml supplies Python metadata under PEP 621. The README states that maturin merges metadata from Cargo.toml and pyproject.toml, with pyproject.toml taking precedence. That ordering matters in practice: a version bump in Cargo.toml can be silently overridden if the same field is pinned in pyproject.toml.
Naming follows the crate layout rather than a separate setting. The package name is the name field in the [package] section of Cargo.toml. The importable module name is the name value in the [lib] section, which defaults to the package name. For binaries, the module name is simply the name of the binary cargo produces. If you want the Python package and the import name to differ, you set module-name in [tool.maturin], and the README's mixed-project example uses module-name = "my_project._lib_name" to nest the extension inside a Python package.
For mixed projects, maturin expects a directory named after the module sitting next to Cargo.toml, containing __init__.py and other Python sources. A different location is configured with tool.maturin.python-source. The README explicitly recommends this structure to avoid a common ImportError pitfall, which is a rare case of a packaging tool documenting a layout choice as a correctness issue rather than a style preference. On maturin develop, the native library is copied into that Python folder, and for cffi the glue code is copied too; the README tells you to add those files to your gitignore.
Installing maturin and running a first develop cycle
The README offers three installation routes: downloading binaries from the latest release, pipx, or uv. It also notes that plain pip install maturin works if you would rather not use an isolated tool installer. The pipx route looks like this.
pipx install maturinThe equivalent with uv is a single command as well.
uv tool install maturinOnce installed, maturin new creates a new cargo project with maturin already configured, which is the fastest way to see the expected file layout. For an existing crate, the inner loop is maturin develop, which builds the crate and installs it as a Python module directly into the current virtualenv. The README warns that develop is faster but does not support everything that installing the built wheel does, so a green develop run is not proof that a pip install of the same code will work.
The published path is maturin build, which writes wheels to target/wheels by default. Both build and develop accept -r or --release for an optimized compile, which you will want for anything you intend to measure. For the mixed layout, the relevant configuration is a pair of keys under [tool.maturin] in pyproject.toml.
[tool.maturin]
python-source = "python"
module-name = "my_project._lib_name"With that in place, the Rust side has to agree on the last segment of the module name. The README shows the pyo3 attribute form explicitly: the #[pymodule] function is annotated with #[pyo3(name="_lib_name")], matching the final component of module-name and not the full dotted path. Getting this wrong produces an import that resolves to the package but not to the extension.
Where maturin stops helping: Linux wheels and platform tags
The hardest part of shipping a compiled Python package is not building it, it is building it in a way that runs on machines other than yours. The README is direct about this: publishing for Linux requires the manylinux docker container or zig, and publishing from a repository can use the PyO3/maturin-action GitHub action. The repository ships a Dockerfile that builds maturin itself against manylinux base images and rust-musl-cross builders, with MANYLINUX defaulting to manylinux2014 and separate stages for x86_64 and aarch64. That is a build matrix, not a single command.
zig is exposed as an optional dependency of the Python package, declared as zig = ["ziglang>=0.10.0"], and patchelf is a second extra. Using either means accepting an extra toolchain in your build environment. This is the point where maturin is the wrong tool for some users: if your project is pure Python, none of this applies and maturin adds a Rust toolchain requirement for no benefit. If you cannot run containers or install Zig in CI, you are limited to whatever platforms your local toolchain can target, and the README does not present a fallback for that case.
There is also a scope boundary in the interpreter support. The README says maturin supports building wheels for Python 3.8+ on Windows, Linux, macOS and FreeBSD, with basic PyPy and GraalPy support. The word basic is doing work there: if your product depends on PyPy or GraalPy, treat the support as a starting point to verify rather than a guarantee. The pyproject.toml in the repository declares requires-python >=3.7 for maturin itself, which is a different constraint from the 3.8+ wheel support described in the README.
maturin against setuptools-rust, and the bootstrap quirk
The obvious alternative is setuptools-rust, which the README mentions twice: maturin does not clash with an existing setuptools-rust configuration, and the repository's own setup.py uses setuptools-rust for bootstrapping. The difference in approach is where the build logic lives. setuptools-rust extends setuptools, so your build is a Python build that happens to invoke cargo, and you keep setup.py and its extension definitions. maturin inverts that: the crate is the source of truth and maturin generates the packaging around it, reading Cargo.toml for names and versions and pyproject.toml for metadata.
That inversion is why maturin can be a single binary with no configuration files in the common case, and also why it is awkward when your project genuinely needs setuptools features that maturin does not model. The repository's own setup.py is an honest illustration of the boundary. Its comments state that maturin is self bootstrapping, but that on platforms like FreeBSD, which are not manylinux or musllinux, pip will try to install maturin from the source distribution, and that distribution cannot depend on maturin. The workaround uses setuptools and setuptools-rust, and the comments say it is only suited to bootstrapping, supporting a wheel build and pip install from the source directory, with maturin sdist recommended for producing maturin's own source distribution. So even the project that replaces setuptools-rust keeps a setuptools-rust path for one platform.
A second alternative is to skip wheels entirely and distribute a binary that users invoke, which is what many Rust tools do. maturin's README anticipates this: it packages Rust binaries as Python packages too, which is useful when your users already have a Python environment and you want the binary delivered through pip rather than through a platform installer.
Maintenance, licence and what upgrading costs you
The repository is not archived, and the last push was on 2026-09-22, so the project is being worked on. Releases are frequent: v1.15.0 on 2026-08-24, v1.14.1 on 2026-06-19, and v1.14.0 on 2026-06-12. That cadence is good for fixes and less good for pinning, because maturin is a build tool that sits in your CI pipeline and a behaviour change in wheel naming or metadata merging shows up as a failed publish rather than a failed test.
The version constraint worth reading before you upgrade is the Rust side. The repository's Cargo.toml sets rust-version = "1.89" and edition = "2024". If your crate is on an older toolchain, the version of maturin you can use is bounded by the toolchain you can install, independent of maturin's own release number. Pinning maturin in CI and bumping it deliberately is the practical approach, since the tool compiles your code with your flags and a change in its defaults is not something your test suite will catch.
On licensing, the GitHub metadata says Apache-2.0, but the repository carries both license-apache and license-mit files, the Cargo.toml declares license = "MIT OR Apache-2.0", and the Python metadata in pyproject.toml declares the same dual licence with license-files listing both. If your compliance process records a single identifier from the repository page, it will disagree with what the manifests say. maturin is a build-time tool rather than a linked library, so the usual question is whether you redistribute it or only invoke it, and that is a question for your own legal review rather than something the README answers.
Editorial conclusion
Adopt maturin if you already have a Rust crate and want pip-installable wheels without maintaining setuptools-rust glue, especially if you only ever ship CPython targets. Skip it if you need a pure-Python wheel, or if you cannot run the manylinux container, Zig or maturin-action in CI for Linux artifacts. Before committing, verify three things in your own checkout: that your pyproject.toml uses the mixed layout with python-source and module-name rather than the flat layout the README flags as an ImportError pitfall, that your CI produces the platform tags you actually intend to publish, and that the licence files match the MIT OR Apache-2.0 declaration when you vendor the tool. The Apache-2.0 label on the repository is only half the story; the Cargo manifest and the Python metadata both say MIT OR Apache-2.0.
Frequently asked questions
How do I install maturin?
The README lists three routes: download a binary from the latest release, run pipx install maturin, or run uv tool install maturin. It also notes that pip install maturin works if you do not want to use an isolated tool installer.
How do I use maturin in a project?
maturin has three main commands: maturin new creates a cargo project with maturin configured, maturin build writes wheels to target/wheels without uploading them, and maturin develop builds the crate and installs it as a module in the current virtualenv. The README recommends publishing with uv publish rather than uploading from maturin.
What is maturin in the context of Python and Rust?
maturin is a tool that builds and publishes crates with pyo3, cffi and uniffi bindings, as well as plain Rust binaries, as Python packages. It supports building wheels for Python 3.8+ on Windows, Linux, macOS and FreeBSD.
What is maturin rust used for?
The README describes maturin as building and publishing crates with pyo3, cffi and uniffi bindings, as well as rust binaries, as python packages with minimal configuration. It reads Cargo.toml for names and versions and pyproject.toml for Python metadata, and can upload wheels to PyPI.
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-maturin)