Skip to content

Fix: Maturin Not Working — develop Errors, ABI3 Wheels, manylinux, and macOS Universal Builds

FixDevs ·

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 develop

Or 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_pkg

Or 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 available

Or 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 develop builds 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 manylinux and macosx_x_y tags, 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 abi3 feature for forward compatibility.
  • pyproject.toml and Cargo.toml disagree. Maturin reads both. The package name in [project] must match the cdylib name in Cargo or your import fails after install.

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 develop

For uv users:

uv venv
source .venv/bin/activate
uv pip install maturin
maturin develop

If 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 --release

Or use the --zig flag to cross-compile without Docker (faster on macOS/Windows hosts):

maturin build --release --target x86_64-unknown-linux-gnu --zig

Verify 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_64

manylinux_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.whl

The 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-darwin

This requires the Rust targets installed:

rustup target add aarch64-apple-darwin x86_64-apple-darwin

Two 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. Use rustls instead when possible, or use vendored features.
MACOSX_DEPLOYMENT_TARGET=11.0 maturin build --release --target universal2-apple-darwin

Fix 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:

  1. [project].name and [lib].name must match — that’s the import name.
  2. [project].version and [package].version should match — Maturin won’t error if they differ but PyPI shows whichever you tag.
  3. 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 --release

This 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 add

Maturin 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_TOKEN

Or 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/*.whl

Use 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 found on Linux. Install build essentials: apt-get install build-essential or dnf install gcc.
  • error: linkerlink.exenot found on Windows. Install Visual Studio Build Tools with the “Desktop development with C++” workload. Rust on Windows uses MSVC by default.
  • develop works but build produces an empty wheel. You have a pyproject.toml [project].name with hyphens, but the Rust lib name uses underscores. Set them consistently — usually underscores in both.
  • Cross-compile fails with ring/openssl symbol errors. These C-dependent crates don’t cross-compile cleanly. Use rustls for TLS, or build natively on each target’s runner.
  • maturin develop --uv fails. That flag requires Maturin 1.5+ and a working uv binary on PATH. Update Maturin.
  • Different SIMD or AVX behavior between dev and prod. You built with -C target-cpu=native on a Zen 4 desktop, then deployed to an older Intel server. Drop the flag for distributable wheels.
  • UnicodeDecodeError reading Cargo.toml on Windows. PowerShell saved the file as UTF-16. Re-save as UTF-8 without BOM.
  • maturin upload 403 from PyPI. Use a project-scoped API token, not your account password. Generate at pypi.org → Account settings → API tokens.

For related Python packaging and build issues, see pip could not build wheels, Python packaging not working, uv not working, and PDM not working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles