TL;DR: PyO3 is the standard Rust crate for Python interoperability in 2026. Use it when you want to speed up Python code with native Rust extensions or embed Python inside a Rust program. Choose PyO3 if you need real Rust performance in a Python ecosystem; choose pure Python only when performance and native packaging are not the bottleneck.
What Is PyO3?
PyO3 is the standard Rust crate for bindings to CPython, and it is the default choice in 2026 when you want Rust and Python to work together in one product.
Two main use cases:
Use case 1: Rust → Python (most common)
Write hot code in Rust → expose as Python module → import in Python
Use case 2: Python → Rust (embedding)
Rust binary → calls Python interpreter → runs Python scriptsThis page matters most for Python engineers hitting performance ceilings, data and ML teams, and Rust developers who need to integrate with Python-heavy environments instead of replacing them outright.
How Do You Create a Python Module in Rust?
Annotate Rust functions with #[pyfunction] and register them with #[pymodule].
# Cargo.toml
[lib]
name = "my_module"
crate-type = ["cdylib"] # required for Python extensions
[dependencies]
pyo3 = { version = "0.23", features = ["extension-module"] }use pyo3::prelude::*;
#[pyfunction]
fn sum_numbers(a: f64, b: f64) -> f64 {
a + b
}
#[pyfunction]
fn fibonacci(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
_ => {
let (mut a, mut b) = (0u64, 1u64);
for _ in 2..=n {
(a, b) = (b, a + b);
}
b
}
}
}
#[pymodule]
fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_numbers, m)?)?;
m.add_function(wrap_pyfunction!(fibonacci, m)?)?;
Ok(())
}When Should You Use PyO3?
Use PyO3 when Python is still part of the product boundary, but pure Python is no longer fast enough for the hottest code paths.
PyO3 is a strong fit when:
- you need a Python package with a Rust performance core
- your users still expect normal
pipinstallation workflows - you are integrating Rust into an existing Python codebase
- you want to keep business logic in Python while moving bottlenecks to Rust
PyO3 is a weaker fit when you want a fully standalone Rust application with no Python dependency surface, or when the problem is mostly I/O-bound and Rust will not materially change outcomes.
How Do You Build and Install the Python Package?
Use maturin; the official build tool for PyO3 projects.
pip install maturin
# New project
maturin new --bindings pyo3 my-module
cd my-module
# Develop mode (build + install in current virtualenv)
maturin develop
# Build a wheel for distribution
maturin build --release
# Publish to PyPI
maturin publishAfter maturin develop:
# Python
import my_module
print(my_module.sum_numbers(3.0, 4.5)) # 7.5
print(my_module.fibonacci(10)) # 55How Do You Expose Rust Structs as Python Classes?
Use #[pyclass] and #[pymethods] to create Python-compatible classes.
use pyo3::prelude::*;
#[pyclass]
struct Point {
#[pyo3(get, set)]
x: f64,
#[pyo3(get, set)]
y: f64,
}
#[pymethods]
impl Point {
#[new]
fn new(x: f64, y: f64) -> Self {
Point { x, y }
}
fn distance_to(&self, other: &Point) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
fn __repr__(&self) -> String {
format!("Point({}, {})", self.x, self.y)
}
}
#[pymodule]
fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Point>()?;
Ok(())
}from my_module import Point
p1 = Point(0.0, 0.0)
p2 = Point(3.0, 4.0)
print(p2.distance_to(p1)) # 5.0
print(repr(p1)) # Point(0, 0)PyO3 vs ctypes or CFFI in 2026
Choose PyO3 in 2026 when you want Rust-native ergonomics and typed CPython bindings; choose ctypes or CFFI mainly for thinner legacy interop layers.
| PyO3 | ctypes / CFFI | |
|---|---|---|
| Primary use | Rust extension modules | Foreign function interop |
| Ergonomics for Rust developers | Strong | Weaker |
| Python packaging story | Better with maturin | More manual |
| Access to Python objects | Native | Lower-level |
| Best fit | Rust-powered Python packages | Thin existing C API bridges |
| 2026 default recommendation | Yes | Only in narrower cases |
If you are building a modern Rust-backed Python package, PyO3 is usually the right choice. If you already have a stable C ABI surface and only need a thin bridge, ctypes or CFFI can still be enough.
What Real-World Projects Use PyO3?
PyO3 matters because some of the most visible high-performance Python tools now rely on Rust under the hood.
| Project | What it does |
|---|---|
pydantic-core | Pydantic v2's validation engine |
ruff | Fast Python linter/formatter |
polars | DataFrame library (Python bindings) |
orjson | Fast JSON library for Python |
cryptography | Cryptographic primitives |
tokenizers (HuggingFace) | Fast tokenization for ML |
That list is the clearest practical signal that PyO3 is not a toy bridge. It is part of the real Python performance toolchain.
Why Does PyO3 Matter Professionally?
PyO3 matters because it lets a Rust engineer create value inside a Python-heavy company without asking the company to rewrite everything.
That makes it especially relevant in data platforms, ML tooling, developer tools, CLI products, and backend services with Python at the edges. Teams often do not need Rust everywhere. They need one performance-critical module to stop being slow, memory-heavy, or unsafe. PyO3 is often the answer.
Frequently Asked Questions
For CPU-bound work: typically 10–100x faster. The speedup depends on the algorithm; Rust eliminates Python's interpreter overhead, dynamic dispatch, and GIL contention. For I/O-bound code, the difference is much smaller.
Yes. PyO3 provides Python<'py> tokens that represent holding the GIL. To release the GIL for parallel Rust code, use py.allow_threads(|| { ... }).
Yes; embed the Python interpreter and call Python code:
Python::with_gil(|py| {
let sys = py.import("sys")?;
let version: String = sys.getattr("version")?.extract()?;
println!("Python {version}");
Ok::<_, PyErr>(())
});maturin is the modern, recommended build tool; it handles everything automatically. setuptools-rust is older and requires more manual configuration. Use maturin for new projects.
Sources
Related Glossary Terms
- WASM: Alternative way to run Rust in non-native environments
- Unsafe: PyO3 internals use unsafe to interface with CPython
- Ownership: PyO3 manages ownership across the Rust/Python boundary
- Polars: The Python-facing Polars ecosystem relies on Rust bindings patterns similar to PyO3
Keep Reading
- Rust for Python Developers: the complete guide to using Rust from Python
- Rust vs Python Performance: why PyO3 exists: Python speed limits
- Rust vs Python in 2026: when to use each language
