Rust Extensions Advanced¶
When you'd use this
PyO3, maturin, memory safety, GIL management and high-performance Python extensions.
Write performance-critical or safety-critical pieces in Rust and call them from Python via PyO3.
Why Rust for Python extensions?¶
A core question explored in Rust Extensions: Why Rust for Python extensions.
| Advantage | Explanation |
|---|---|
| Memory safety | No segfaults, no buffer overflows (compile-time guarantees) |
| No GC | Predictable performance, no pause spikes |
| C-level speed | Compiled to native code via LLVM |
| Great tooling | cargo, clippy, rustfmt — modern dev experience |
| Easy Python bindings | PyO3 makes it seamless |
| Thread-safe | Ownership model prevents data races |
Quick start with maturin¶
Build and install a Rust-backed Python module with one tool.
# Install tools
pip install maturin
cargo install cargo-expand # optional: inspect macro expansions
# Create project
mkdir my_rust_module && cd my_rust_module
maturin init --bindings pyo3
# Project structure:
# my_rust_module/
# ├── Cargo.toml
# ├── pyproject.toml
# └── src/
# └── lib.rs
Cargo.toml¶
[package]
name = "my_rust_module"
version = "0.1.0"
edition = "2021"
[lib]
name = "my_rust_module"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.22", features = ["extension-module"] }
numpy = "0.22" # for NumPy integration
Basic functions¶
Expose Rust functions to Python with PyO3 attributes.
// src/lib.rs
use pyo3::prelude::*;
/// Compute the sum of squares (10-100x faster than Python)
#[pyfunction]
fn sum_of_squares(n: u64) -> u64 {
(1..=n).map(|i| i * i).sum()
}
/// Fibonacci with memoization
#[pyfunction]
fn fibonacci(n: u64) -> u64 {
let mut a: u64 = 0;
let mut b: u64 = 1;
for _ in 0..n {
let tmp = a + b;
a = b;
b = tmp;
}
a
}
/// String processing
#[pyfunction]
fn count_vowels(s: &str) -> usize {
s.chars()
.filter(|c| "aeiouAEIOU".contains(*c))
.count()
}
/// Register all functions in the module
#[pymodule]
fn my_rust_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_of_squares, m)?)?;
m.add_function(wrap_pyfunction!(fibonacci, m)?)?;
m.add_function(wrap_pyfunction!(count_vowels, m)?)?;
Ok(())
}
Build and use:¶
import my_rust_module
print(my_rust_module.sum_of_squares(1_000_000)) # instant!
print(my_rust_module.fibonacci(50)) # 12586269025
print(my_rust_module.count_vowels("Hello World")) # 3
Python classes in Rust¶
Implement Python classes in Rust for performance-critical objects.
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(&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)
}
fn __add__(&self, other: &Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
from my_rust_module import Point
p1 = Point(3.0, 4.0)
p2 = Point(6.0, 8.0)
print(p1.distance(p2)) # 5.0
print(p1 + p2) # Point(9.0, 12.0)
print(p1.x, p1.y) # 3.0 4.0
NumPy integration¶
Operate on NumPy arrays from Rust without copying.
use numpy::{IntoPyArray, PyArrayDyn, PyReadonlyArrayDyn};
use pyo3::prelude::*;
use ndarray::ArrayD;
#[pyfunction]
fn double_array<'py>(
py: Python<'py>,
arr: PyReadonlyArrayDyn<'py, f64>,
) -> Bound<'py, PyArrayDyn<f64>> {
let input = arr.as_array();
let output: ArrayD<f64> = &input * 2.0;
output.into_pyarray_bound(py)
}
#[pyfunction]
fn element_wise_sqrt<'py>(
py: Python<'py>,
arr: PyReadonlyArrayDyn<'py, f64>,
) -> Bound<'py, PyArrayDyn<f64>> {
let input = arr.as_array();
let output = input.mapv(|x| x.sqrt());
output.into_pyarray_bound(py)
}
import numpy as np
from my_rust_module import double_array, element_wise_sqrt
arr = np.array([1.0, 4.0, 9.0, 16.0])
print(double_array(arr)) # [2., 8., 18., 32.]
print(element_wise_sqrt(arr)) # [1., 2., 3., 4.]
Releasing the GIL¶
Drop the GIL during heavy Rust work so Python threads can run.
use pyo3::prelude::*;
#[pyfunction]
fn cpu_intensive(py: Python<'_>, n: u64) -> u64 {
// Release the GIL — other Python threads can run!
py.allow_threads(|| {
// Pure Rust computation — no Python objects allowed here
(1..=n).map(|i| i * i).sum()
})
}
This means Python threads can run concurrently while Rust does heavy computation.
Error handling¶
Map Rust Result errors to Python exceptions cleanly.
use pyo3::prelude::*;
use pyo3::exceptions::PyValueError;
#[pyfunction]
fn divide(a: f64, b: f64) -> PyResult<f64> {
if b == 0.0 {
Err(PyValueError::new_err("Division by zero"))
} else {
Ok(a / b)
}
}
from my_rust_module import divide
print(divide(10.0, 3.0)) # 3.333...
try:
divide(1.0, 0.0)
except ValueError as e:
print(e) # Division by zero
Async Rust functions¶
Async Rust functions in Rust Extensions — what it is and when to use it.
use pyo3::prelude::*;
use pyo3_asyncio_0_21::tokio::future_into_py;
#[pyfunction]
fn async_fetch(py: Python<'_>, url: String) -> PyResult<Bound<'_, PyAny>> {
future_into_py(py, async move {
let body = reqwest::get(&url)
.await
.map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string()))?
.text()
.await
.map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string()))?;
Ok(body)
})
}
Performance comparison¶
Performance comparison in Rust Extensions — what it is and when to use it.
import timeit
import my_rust_module
# Fibonacci benchmark
print("Python:", timeit.timeit("fib_py(30)", globals=globals(), number=100000))
print("Rust: ", timeit.timeit("my_rust_module.fibonacci(30)", globals=globals(), number=100000))
# Python: ~4.5s
# Rust: ~0.03s (150x faster)
Publishing to PyPI¶
Publishing to PyPI in Rust Extensions — what it is and when to use it.
# Build wheels for all platforms
maturin build --release
# Or publish directly
maturin publish # uploads to PyPI
# CI/CD — use maturin-action in GitHub Actions
Practice Exercises¶
- Write a Rust function that finds all prime numbers below N using a sieve.
- Create a Rust class with Python-accessible methods and properties.
- Process a NumPy array in Rust — normalize (zero mean, unit std) each column.
- Release the GIL in a CPU-heavy function and verify Python threads run concurrently.
- Benchmark the same algorithm in Python, Cython, Numba and Rust — compare speeds.
- Publish your Rust extension to TestPyPI with maturin.
💬 Discussion
Have a question about this topic? Found an error? Share your thoughts below.