Fix: Maturin Not Working — develop Errors, ABI3 Wheels, manylinux, and macOS Universal Builds
Part of: Python Errors
Quick Answer
How to fix Maturin errors — maturin develop fails outside venv, abi3 forward compatibility, manylinux wheel auditing, macOS universal2 cross-compile, pyproject.toml vs Cargo.toml conflicts, and PyO3 feature flags.
The Error
You run maturin develop in your project and get this:
$ maturin develop
💥 maturin failed
Caused by: You need to be inside a virtualenv or conda environment to use developOr maturin build succeeds, you upload to PyPI, and users on Linux can’t install:
ERROR: Could not find a version that satisfies the requirement my_pkg
ERROR: No matching distribution found for my_pkgOr PyO3 refuses to compile against the Python you have:
error: the configured Python interpreter version (3.13) is newer than PyO3's maximum supported version (3.12)
help: please check if an updated version of PyO3 is availableOr your macOS wheel works on the build machine but fails on a different Mac:
ImportError: dlopen(...): symbol not found in flat namespace '_PyObject_...'Why This Happens
Maturin orchestrates three things at once: the Rust build (via Cargo), the Python wheel format, and the platform’s ABI rules. Most failures map to one of these layers:
- Wrong runtime for
develop.maturin developbuilds and installs into a Python environment. It refuses to install into the system Python because that path leads to permission errors and breaks system packages. You need an active venv or conda env. - Platform-specific wheels that aren’t tagged correctly. A wheel built on Ubuntu 24.04 isn’t installable on Ubuntu 20.04 (different glibc), and a wheel built on macOS 14 isn’t installable on macOS 11. The fix is
manylinuxandmacosx_x_ytags, which Maturin handles when you ask for them. - PyO3 version vs Python version mismatch. PyO3 has a maximum Python version it knows about. Build a wheel against Python 3.13 with an old PyO3, and it refuses. Upgrade PyO3, or use the
abi3feature for forward compatibility. pyproject.tomlandCargo.tomldisagree. Maturin reads both. The package name in[project]must match the cdylib name in Cargo or your import fails after install.
The second layer worth understanding is how Maturin invokes Cargo. Maturin shells out to cargo build with specific environment variables (PYO3_PYTHON, PYO3_CONFIG_FILE) and a target triple inferred from your active Python and the --target flag. When something fails inside that subprocess, the error message often surfaces as a Cargo or linker error, not a Maturin error, which makes the actual cause look unrelated. If you see a linker or cc failure, read the line above the failure — Maturin will have logged the cargo invocation it used, and you can rerun it manually with -vv for full output.
The third source of friction is platform tag negotiation. Pip on the install side checks the wheel filename against a hardcoded list of compatible tags for the running interpreter. If your wheel is named linux_x86_64 (the unrestricted tag), pip considers it incompatible with every public Linux distribution by default — only an exact match counts. The manylinux standard exists precisely so that one wheel filename declares compatibility with a whole class of glibc versions. Maturin only emits the correct manylinux tag if it’s run inside a manylinux container or it can prove the wheel meets that ABI (via auditwheel repair).
Version History That Changes the Failure Mode
Maturin’s behavior has shifted enough across versions that an error you hit on one version may not reproduce on another. The major inflection points:
- 0.13 (Aug 2022): Last pre-1.0 series. API and CLI flags churned regularly. If you see tutorials referencing
--cargo-extra-argsor--rustc-extra-args, they’re for this era. - 1.0 (Mar 2023): First stable release. CLI surface locked in, wheel naming standardized, and
maturin developgained the strict venv check that produces the “You need to be inside a virtualenv” error referenced above. Earlier versions silently installed into the system Python. - 1.4 (early 2024): macOS
universal2-apple-darwintarget became reliable in CI. Before 1.4, the universal2 build required manuallipostitching of two single-arch wheels; after 1.4, a singlematurin build --target universal2-apple-darwininvocation works end-to-end. - 1.5 (mid 2024): Default Linux image bumped to
manylinux_2_28for new wheels (glibc 2.28, RHEL 8 baseline). Wheels built on the new default no longer install on CentOS 7 or Ubuntu 18.04 unless you explicitly downgrade with--manylinux 2014. - 1.7 (late 2024): PyO3 0.22+ compatibility was added, including the new Bound API and Python 3.13 support. Pinning
maturin = "1.6"while bumping PyO3 to 0.22 is a recipe for build-script failures. - 1.8 (late 2024): First-class Cargo workspace support — Maturin can now publish multiple crates from a single workspace without per-crate
pyproject.tomlfiles. Before 1.8, workspace setups needed manual[tool.maturin.python-source]overrides.
When you hit an error, check maturin --version first. If you’re on 1.4 or earlier, upgrading often fixes the issue without any other change.
Fix 1: Activate a Venv Before develop
maturin develop writes the compiled .so / .pyd into whichever Python environment is active. It needs one:
python -m venv .venv
source .venv/bin/activate # Linux/macOS
.venv\Scripts\activate # Windows PowerShell
pip install maturin
maturin developFor uv users:
uv venv
source .venv/bin/activate
uv pip install maturin
maturin developIf you’re using a global tool like pipx install maturin, that’s fine for the binary — but you still need a venv where the built module gets installed. maturin develop always installs into the active Python interpreter, not into the env that owns the maturin binary.
Pro Tip: maturin develop --release builds with optimizations on. Default is debug mode, which can be 10-100x slower at runtime. Always benchmark with --release.
Fix 2: Use abi3 for Forward-Compatible Wheels
Without abi3, you need to build a separate wheel for every Python version (3.10, 3.11, 3.12, 3.13). With abi3, one wheel works across many versions — at the cost of using only the stable Python C API.
In Cargo.toml:
[lib]
name = "my_pkg"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.22", features = ["extension-module", "abi3-py310"] }abi3-py310 means “minimum Python 3.10, works on every Python ≥ 3.10.” Maturin picks up the feature flag and produces a wheel like my_pkg-0.1.0-cp310-abi3-linux_x86_64.whl instead of cp312-cp312-linux_x86_64.whl.
Trade-off: abi3 disallows some PyO3 features that need the unstable API (a few macros, certain protocol implementations). The PyO3 docs list what’s missing — for most numeric/data libraries it’s fine.
Fix 3: Build manylinux Wheels for Linux
A wheel built directly on your Ubuntu workstation usually isn’t installable elsewhere because it links against your specific glibc. You need a manylinux wheel, built inside a special container:
docker run --rm -v $(pwd):/io ghcr.io/pyo3/maturin build --releaseOr use the --zig flag to cross-compile without Docker (faster on macOS/Windows hosts):
maturin build --release --target x86_64-unknown-linux-gnu --zigVerify the wheel’s manylinux tag is correct:
unzip -p target/wheels/*.whl my_pkg-0.1.0.dist-info/WHEEL | grep Tag
# Tag: cp310-abi3-manylinux_2_17_x86_64manylinux_2_17 means “works on any Linux with glibc 2.17+” (which is essentially every Linux distro from 2014 onward).
Common Mistake: Tagging a wheel as manylinux2014 when it actually links against a newer glibc. Use auditwheel to check (and repair, if possible):
auditwheel show target/wheels/my_pkg-0.1.0-cp310-abi3-linux_x86_64.whl
auditwheel repair target/wheels/my_pkg-0.1.0-cp310-abi3-linux_x86_64.whlThe Maturin Docker image runs auditwheel automatically.
Fix 4: macOS Universal2 (Apple Silicon + Intel)
For wheels that run on both Apple Silicon and Intel Macs, build a universal2 wheel:
maturin build --release --target universal2-apple-darwinThis requires the Rust targets installed:
rustup target add aarch64-apple-darwin x86_64-apple-darwinTwo issues that bite:
- macOS deployment target. Set
MACOSX_DEPLOYMENT_TARGET=11.0(or whatever minimum you want to support) before building. Without it, your wheel only works on the macOS version you built on. - Dependencies in Cargo.toml that need C libs. If your crate depends on, say,
openssl-sys, the universal2 build needs both arches of OpenSSL available. Userustlsinstead when possible, or usevendoredfeatures.
MACOSX_DEPLOYMENT_TARGET=11.0 maturin build --release --target universal2-apple-darwinFix 5: Align pyproject.toml and Cargo.toml
Maturin reads metadata from both files. The package name must agree:
# pyproject.toml
[project]
name = "my_pkg"
version = "0.1.0"
requires-python = ">=3.10"
[build-system]
requires = ["maturin>=1.7,<2.0"]
build-backend = "maturin"
[tool.maturin]
features = ["pyo3/extension-module"]# Cargo.toml
[package]
name = "my_pkg"
version = "0.1.0"
edition = "2021"
[lib]
name = "my_pkg"
crate-type = ["cdylib"]Three rules:
[project].nameand[lib].namemust match — that’s the import name.[project].versionand[package].versionshould match — Maturin won’t error if they differ but PyPI shows whichever you tag.crate-type = ["cdylib"]is required. Without it Cargo produces an executable, not a Python-loadable shared library.
Note: For a pure-Rust extension (no Python source), the layout is src/lib.rs. For mixed Rust + Python, put Python code in python/my_pkg/__init__.py and set python-source = "python" under [tool.maturin].
Fix 6: Upgrade PyO3 for Newer Python Versions
When PyO3 fails on a new Python release:
error: the configured Python interpreter version (3.13) is newer than PyO3's maximum supported version (3.12)Check PyO3’s release notes for the version that added support, and update Cargo.toml:
[dependencies]
pyo3 = { version = "0.22.4", features = ["extension-module", "abi3-py310"] }For a temporary unblock while waiting for a new PyO3 release, set:
export PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1
maturin build --releaseThis tells PyO3 to compile against the stable ABI and trust that future Python versions are compatible. It works for abi3 builds where you’ve stayed within the stable API.
Fix 7: Wheel Imports But Functions Are Missing
You install your wheel, import my_pkg works, but my_pkg.some_function() raises AttributeError. Two common causes:
The Rust function isn’t #[pyfunction] and isn’t registered. PyO3 only exposes what you tell it to:
use pyo3::prelude::*;
#[pyfunction]
fn add(a: i64, b: i64) -> i64 {
a + b
}
#[pymodule]
fn my_pkg(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(add, m)?)?;
Ok(())
}The #[pymodule] function name must match the package name (my_pkg here). Mismatched names → “module not found” or partial imports.
You have a Python __init__.py shadowing the Rust module. With a mixed layout, the import resolution is: Python finds python/my_pkg/__init__.py first, and it must re-export from the Rust extension:
# python/my_pkg/__init__.py
from .my_pkg import *
from .my_pkg import addMaturin builds the Rust extension as my_pkg.my_pkg in this layout — so __init__.py has to bridge it.
Fix 8: Publishing to PyPI
Once your wheels build cleanly, publish:
maturin publish --username __token__ --password $PYPI_TOKENOr in two steps — build then upload with twine:
maturin build --release --target x86_64-unknown-linux-gnu --zig
maturin build --release --target aarch64-unknown-linux-gnu --zig
maturin build --release --target universal2-apple-darwin
maturin build --release --target x86_64-pc-windows-msvc
twine upload target/wheels/*.whlUse GitHub Actions for the full matrix. Maturin’s docs ship a workflow template that produces wheels for all major platforms — copy that as the starting point.
Pro Tip: Always upload an sdist too: maturin sdist. If a user’s platform isn’t covered by your wheels, pip falls back to the sdist and compiles from source. Without it, niche platforms see “no matching distribution.”
Still Not Working?
A few less-obvious failures:
linker 'cc' not foundon Linux. Install build essentials:apt-get install build-essentialordnf install gcc.error: linkerlink.exenot foundon Windows. Install Visual Studio Build Tools with the “Desktop development with C++” workload. Rust on Windows uses MSVC by default.developworks butbuildproduces an empty wheel. You have apyproject.toml[project].namewith hyphens, but the Rust lib name uses underscores. Set them consistently — usually underscores in both.- Cross-compile fails with
ring/opensslsymbol errors. These C-dependent crates don’t cross-compile cleanly. Userustlsfor TLS, or build natively on each target’s runner. maturin develop --uvfails. That flag requires Maturin 1.5+ and a workinguvbinary on PATH. Update Maturin.- Different SIMD or AVX behavior between dev and prod. You built with
-C target-cpu=nativeon a Zen 4 desktop, then deployed to an older Intel server. Drop the flag for distributable wheels. UnicodeDecodeErrorreadingCargo.tomlon Windows. PowerShell saved the file as UTF-16. Re-save as UTF-8 without BOM.maturin upload403 from PyPI. Use a project-scoped API token, not your account password. Generate at pypi.org → Account settings → API tokens.abi3wheel imports butTypeError: argument 'x' must be int, not int. You bumped PyO3 across the 0.20→0.22 boundary without rebuilding all dependent crates. The Bound API changed how integer extraction behaves at the boundary. Wipetarget/and rebuild from a clean state.maturin developworks in CI but the wheel is missing from artifacts.developinstalls in place and doesn’t emit a.whlfile. Switch tomaturin build --release(ormaturin build --release --out dist/) for any pipeline step that needs to upload an artifact.error: failed to run custom build command for 'pyo3-build-config'. PyO3’s build script can’t locate the active Python. SetPYO3_PYTHONexplicitly to the interpreter path, especially in Docker images where multiple Python versions coexist.- Wheel filename ends in
linux_x86_64instead ofmanylinux_.... Maturin couldn’t prove the wheel meets a manylinux ABI. Either run inside the official maturin Docker image or pass--manylinux 2_28(and accept the responsibility of verifying that your dependencies actually meet that target).
For related Python packaging and build issues, see pip could not build wheels, Python packaging not working, uv not working, and PDM not working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: PyO3 Not Working — Bound API Migration, GIL Acquisition, Error Conversion, and NumPy Interop
How to fix PyO3 errors — &PyAny vs Bound<PyAny> migration, GIL acquire/release patterns, returning Rust errors as Python exceptions, numpy ndarray zero-copy, pyclass frozen, and async tokio integration.
Fix: Pipenv Not Working — Lock File Generation, Shell Activation, and Dependency Resolution
How to fix Pipenv errors — pipenv lock takes forever, Pipfile.lock not generated, shell activation broken, no virtualenv created, dependency conflict, and migration to uv or Poetry.
Fix: PDM Not Working — Lock File Errors, PEP 582 Confusion, and Script Issues
How to fix PDM errors — pdm install fails resolving dependencies, lock file outdated warning, __pypackages__ vs venv confusion, pdm run script not found, build backend missing, and dependency groups setup.
Fix: Hatch Not Working — Environment Errors, Build Backend, and pyproject.toml Issues
How to fix Hatch errors — hatch env create fails, scripts not found, build backend hatchling missing, version not detected, plugin install errors, and publishing to PyPI.