Python Maturin Build Tool — Deep Dive
Maturin is not merely a convenience wrapper around cargo build. It is a full PEP 517 build backend that understands Rust’s compilation model, Python’s packaging standards, and the platform-specific constraints of distributing native code. Getting the most out of it requires understanding its configuration surface and CI integration patterns.
Build Pipeline Internals
When you run maturin build, the following happens:
- Metadata extraction: Maturin reads
Cargo.tomlfor the crate name and version, andpyproject.tomlfor Python-specific metadata (name, requires-python, classifiers). - Rust compilation: Invokes
cargo rustcwith appropriate flags (--crate-type cdylibfor shared libraries). - Name mangling: Renames the compiled artifact to match Python’s naming convention (e.g.,
my_package.cpython-312-x86_64-linux-gnu.so). - Wheel assembly: Creates a
.whl(ZIP) file containing the shared library, any pure Python files, and PEP 427 metadata. - Platform tagging: Applies correct tags (
manylinux_2_17,macosx_11_0_arm64,win_amd64) so pip knows which wheel fits which system.
Advanced Configuration
pyproject.toml Options
[tool.maturin]
# Include Python source alongside native module
python-source = "python"
# Specify which features to enable in the Rust crate
features = ["pyo3/extension-module", "simd-acceleration"]
# Strip debug symbols for smaller wheels
strip = true
# Module name if different from crate name
module-name = "my_package._native"
# Compatibility level for Linux wheels
manylinux = "2_17"
# Include additional files in the wheel
include = ["LICENSE", "py.typed"]
Cargo.toml Crate Configuration
[lib]
name = "my_package"
crate-type = ["cdylib"] # Required for Python shared library
[dependencies]
pyo3 = { version = "0.22", features = ["extension-module"] }
[profile.release]
lto = true # Link-time optimization for smaller, faster binaries
codegen-units = 1 # Better optimization at the cost of compile time
strip = "symbols" # Remove debug symbols
The extension-module feature is critical: it tells PyO3 not to link against libpython, which would break on many Linux distributions where Python is built without a shared library.
Mixed Rust + Python Architecture
For packages that need both native speed and Pythonic APIs:
my-package/
├── Cargo.toml
├── pyproject.toml
├── src/
│ └── lib.rs # Low-level Rust: _native module
├── python/
│ └── my_package/
│ ├── __init__.py # Re-exports with Pythonic names
│ ├── _native.pyi # Type stubs for IDE support
│ ├── utils.py # Pure Python utilities
│ └── py.typed # PEP 561 marker
In __init__.py:
from my_package._native import FastProcessor # Rust class
from my_package.utils import format_result # Python helper
__all__ = ["FastProcessor", "format_result"]
Type stubs (.pyi files) are essential for IDE autocompletion and mypy checking. Maturin doesn’t generate them — you write them by hand or use pyo3-stub-gen.
CI/CD Strategies
GitHub Actions with maturin-action
name: Build Wheels
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ["3.10", "3.11", "3.12", "3.13"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: PyO3/maturin-action@v1
with:
command: build
args: --release --interpreter python${{ matrix.python }}
- uses: actions/upload-artifact@v4
with:
name: wheel-${{ matrix.os }}-${{ matrix.python }}
path: target/wheels/*.whl
publish:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/download-artifact@v4
- uses: PyO3/maturin-action@v1
with:
command: upload
args: --non-interactive */*.whl
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
Linux: manylinux Compliance
Linux wheels must be built against old-enough glibc to run on most distributions. Maturin uses manylinux Docker images automatically:
# Build manylinux wheels locally via Docker
maturin build --release --manylinux 2_17
The manylinux_2_17 tag (formerly manylinux2014) targets glibc 2.17, covering CentOS 7+ and most modern distros.
macOS: Universal2 Wheels
For Apple Silicon and Intel compatibility:
maturin build --release --target universal2-apple-darwin
This creates a fat binary containing both x86_64 and arm64 code.
Source Distribution (sdist)
Maturin can also produce source distributions for users who want to compile from source:
maturin sdist
The sdist includes Rust source code and Cargo.toml. The user needs a Rust toolchain installed to build from source. This is the fallback when no pre-built wheel matches their platform.
Development Workflow Optimization
Fast Iteration with maturin develop
# Install in development mode (debug build, fast compilation)
maturin develop
# With release optimizations (slower to compile, faster to run)
maturin develop --release
# Skip full rebuild if only Python files changed
maturin develop --skip-install
Integration with pytest
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
# Ensure the native module is built before tests run
[tool.maturin]
python-source = "python"
Run maturin develop && pytest as your test loop. Some teams add a conftest.py that calls maturin develop automatically via subprocess if the .so is stale.
Workspace Support
For monorepos with multiple Rust crates producing Python packages:
# Cargo.toml (workspace root)
[workspace]
members = ["crates/fast-math", "crates/fast-io"]
# Each member has its own pyproject.toml
Build individual members: maturin build -m crates/fast-math/Cargo.toml.
Troubleshooting Common Issues
| Problem | Cause | Fix |
|---|---|---|
ImportError: symbol not found | Missing extension-module feature | Add pyo3/extension-module to features |
| Wrong Python version in wheel name | Maturin detected wrong interpreter | Use --interpreter flag explicitly |
manylinux audit failure | Linked against too-new glibc symbol | Use --manylinux 2_17 or check C dependencies |
| Huge wheel size | Debug symbols included | Add strip = true in [tool.maturin] |
cargo not found in CI | Rust toolchain not installed | Add actions-rust-lang/setup-rust-toolchain step |
One Thing to Remember
Maturin is the complete build-and-ship pipeline for Rust-Python packages. It handles the hardest parts — platform-specific compilation, ABI tagging, manylinux compliance, and PyPI uploads — letting you focus on writing Rust code that makes Python faster.
See Also
- Python Boost Python Bindings Boost.Python lets C++ code talk to Python using clever C++ tricks, like teaching two people to understand each other through a shared phrasebook.
- Python Buffer Protocol The buffer protocol lets Python objects share raw memory without copying, like passing a notebook around the table instead of photocopying every page.
- Python Capsule Api Python Capsules let C extensions secretly pass pointers to each other through Python, like friends passing a sealed envelope through a mailbox.
- Python Cffi Bindings CFFI lets Python talk to fast C libraries, like giving your app a translator that speaks both languages at the same table.
- Python Extension Modules Api The C Extension API is how Python lets you plug in hand-built C code, like adding a turbo engine under your Python program's hood.