Cloud Parts

PyO3

Rust bindings for Python: attributes such as #[pyfunction] and #[pyclass] turn Rust code into a CPython extension module, and the same crate can embed a Python interpreter in a Rust program.

Type
Open source
Made by
PyO3 project and contributors
License
Apache 2.0 or MIT, at your option
Facts checked
9 October 2026

What it is

A Python extension module is a shared library that CPython loads on import and talks to through its C API. PyO3 writes that glue. You mark a Rust module with #[pymodule], functions with #[pyfunction] and structs with #[pyclass], and its procedural macros generate the rest: parsing Python arguments into Rust types (a str into a &str, a list into a Vec<T>), converting return values back, and raising a Python exception when a function returns Err. A Rust panic surfaces in Python as PanicException instead of unwinding through C. Code that works with Python objects directly does it through a Python<'py> token, obtained with Python::attach, and smart pointers such as Bound<'py, T> that borrow its lifetime, so the compiler won’t let a thread touch Python objects unless it is attached to the interpreter. Instances of a #[pyclass] get borrow checking at run time, much like RefCell, because once Python owns an object Rust’s compile-time checker can’t see who else holds it.

Long Rust work can call py.detach(|| ...) to release the GIL so other Python threads run meanwhile. PyO3 has built modules for free-threaded CPython since 0.23, and since 0.28 it assumes they are thread-safe: a module that relies on the GIL declares #[pymodule(gil_used = true)], and the interpreter then re-enables the GIL when it is imported. The abi3 features compile against CPython’s limited API, so one wheel works on every later 3.x release; 0.29 (June 2026) added abi3t, the stable ABI for the free-threaded build from Python 3.15. PyO3 supports CPython 3.9 and newer, PyPy and GraalPy, needs Rust 1.85, and is usually built and packaged with maturin. Polars, pydantic-core, cryptography, orjson, tokenizers and tiktoken all use it.

Why teams pick it

  • Rust functions and structs become Python functions and classes through attributes; argument parsing, type conversion and exception mapping are generated
  • Python’s rules are checked by the Rust compiler: touching Python objects needs the attached-interpreter token, and #[pyclass] types must be Send because any Python thread may use or drop them
  • Real parallelism: detach releases the GIL around Rust work, and modules are ready for free-threaded Python by default since 0.28
  • Fewer wheels with the abi3 and abi3t stable-ABI features, and packaging handled by maturin or setuptools-rust

Watch out for

  • Conversions cost time: a Vec<i32> argument type-checks and converts the whole list on every call, while Python-native types such as Bound<PyList> are almost free to accept but keep Python’s dynamic typing
  • A #[pyclass] can’t have lifetime or generic parameters, so borrowed or generic Rust types need wrapper structs, one per concrete type
  • The API still moves: 0.21 replaced GIL-bound references with Bound, 0.26 renamed with_gil and allow_threads to attach and detach, and 0.28 flipped the free-threading default, so upgrades take migration work
  • A compiled extension needs a wheel for every OS and CPU (and every Python version without abi3), and anyone installing from source needs a Rust toolchain

The PyO3 family

1 technologies built from or on PyO3

How it fits with other technologies

A Rust struct used as a Python class (src/lib.rs of a maturin project named counter) · rust

use pyo3::prelude::*;

#[pymodule]
mod counter {
    use pyo3::exceptions::PyValueError;
    use pyo3::prelude::*;
    use std::collections::HashMap;

    /// A Rust struct that Python sees as a class.
    #[pyclass]
    struct WordCounter {
        counts: HashMap<String, u64>,
    }

    #[pymethods]
    impl WordCounter {
        #[new]
        fn new() -> Self {
            WordCounter { counts: HashMap::new() }
        }

        /// `text` arrives as a &str; the GIL is released while Rust splits it.
        fn feed(&mut self, py: Python<'_>, text: &str) {
            let words: Vec<String> =
                py.detach(|| text.split_whitespace().map(str::to_lowercase).collect());
            for w in words {
                *self.counts.entry(w).or_insert(0) += 1;
            }
        }

        /// Returned as a Python list of (str, int) tuples; Err becomes an exception.
        fn top(&self, n: usize) -> PyResult<Vec<(String, u64)>> {
            if n == 0 {
                return Err(PyValueError::new_err("n must be at least 1"));
            }
            let mut v: Vec<_> = self.counts.iter().map(|(w, c)| (w.clone(), *c)).collect();
            v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
            v.truncate(n);
            Ok(v)
        }
    }
}

Using it from Python#

After maturin develop in the project directory (PyO3 0.29.3, CPython 3.14).

python
from counter import WordCounter

c = WordCounter()
c.feed("the cat and the hat")
c.feed("The end")
print(c.top(2))  # [('the', 3), ('and', 1)]

try:
    c.top(0)
except ValueError as e:
    print(e)  # n must be at least 1

try:
    c.feed(42)  # PyO3 checks the argument type before Rust runs
except TypeError as e:
    print(e)  # 'int' object is not an instance of 'str'

Ways to put Rust or C behind a Python import#

PyO3 is one of several routes. They differ in who writes the glue and what the result depends on.

PyO3CFFIctypes
Native sideRustAny library with a C API; Rust through extern "C"Any shared library with C functions
Glue written byPyO3’s macros, from Rust attributesCFFI, from C declarations you paste inYou, in Python, at run time
Python objects in native codeYes: classes, exceptions and iterators, held as Bound valuesNo: C types onlyNo: C types only
Needs a compiler at installYes, unless a wheel matchesAPI mode yes, ABI mode noNo
  • maturin builds wheels for PyO3 and CFFI bindings alike, and for UniFFI, which generates ctypes bindings from an interface definition.

See it in a real stack

No architecture uses PyO3 yet. Drop it into the playground and wire it up yourself.Open playground →
Where it came from2015–2026

Before PyO3 there was rust-cpython, Daniel Grunwald’s Rust bindings for the Python interpreter, copyrighted from 2015. It declared Python classes through py_class!, a macro_rules! language of its own, and handed Rust code owned Python objects that needed the GIL even to drop. In May 2017 Nikolay Kim was committing to a fork of it, and PyO3 0.1.0 came out on 23 July 2017. When a user asked that day why the fork existed, the reply listed what had changed: procedural macros and specialization instead of a macro language that was “very hard to extend”, Rust structs left as they were, and APIs that returned references bounded by the GIL’s lifetime.

Those features needed nightly Rust, which kept PyO3 out of many production builds until 0.11 in June 2020 added support for stable Rust 1.39. A family of tools grew around it under the PyO3 organisation: pyo3-pack (renamed maturin in 2019) to build and publish wheels, and setuptools-rust for existing setup.py projects. rust-cpython’s README now tells users to switch to PyO3, whose guide describes the two as “fundamentally different” today.

The largest recent changes follow CPython’s move away from the GIL. 0.21 (March 2024) introduced the Bound smart-pointer API in place of references tied to the GIL. 0.23 (November 2024) added support for free-threaded Python 3.13t. 0.26 (August 2025) renamed Python::with_gil and allow_threads to attach and detach, the same words CPython’s docs use for a thread state, and 0.28 (February 2026) made free-threading support opt-out instead of opt-in.

Why it’s built this way

Procedural macros instead of a macro language
rust-cpython’s py_class! defined classes in a macro language its users found hard to extend, for example with async support. PyO3’s attributes sit on ordinary Rust structs and functions and leave them unchanged, so the same code stays usable from Rust.
Model the GIL in Rust’s type system
Python objects may only be touched while the thread is attached to the interpreter. Tying object handles to a Python<'py> lifetime turns that rule into a compile-time check. On free-threaded Python the token no longer means exclusive access, so PyO3’s guide warns that unsafe code which used it as a lock has to be audited.
Borrow checking at run time for Python-owned structs
Once a #[pyclass] object belongs to Python, any number of Python references can reach it, which Rust’s compile-time borrow checker can’t track. PyO3 tracks shared and mutable borrows at run time, like RefCell; a class declared frozen opts out and manages its own interior mutability.
Build tooling outside the library
Building an extension means compiling a cdylib, setting PYO3_BUILD_EXTENSION_MODULE, passing extra linker arguments on macOS and not linking libpython for manylinux wheels. maturin and setuptools-rust do all of this automatically, so PyO3 itself stays an ordinary Cargo dependency.

Timeline

  1. 2015

    Daniel Grunwald’s rust-cpython brings Rust bindings to Python

  2. 2017

    PyO3 forks rust-cpython; 0.1.0 released on 23 July, nightly Rust only

  3. 2020

    0.11 (June) works on stable Rust; 0.13 (December) adds abi3 wheels

  4. 2024

    0.21 (March) introduces the Bound API; 0.23 (November) supports free-threaded Python

  5. 2025

    0.26 (August) renames GIL handling to attach and detach

  6. 2026

    0.28 (February) makes free-threading the default; 0.29 (June) adds the abi3t stable ABI

Sources13 · facts checked 9 October 2026

Improve this page on GitHub