Python stub file (*.pyi) generator for PyO3 with maturin projects.
| crate name | crates.io | docs.rs | doc (main) |
|---|---|---|---|
| pyo3-stub-gen | |||
| pyo3-stub-gen-derive |
[!NOTE] Minimum supported Python version is 3.10. Do not enable 3.9 or older in PyO3 setting.
[!NOTE] Versions 0.15.0–0.17.1 unintentionally included a LGPL dependency. This was removed in 0.17.2, and the affected versions have been yanked.
Our goal is to create a stub file *.pyi from Rust code, however,
automated complete translation is impossible due to the difference between Rust and Python type systems and the limitation of proc-macro.
We take semi-automated approach:
If the default translator does not work, users can specify the translation manually, and these manual translations can be integrated with what the default translator generates. So the users can use the default translator as much as possible and only specify the translation for the edge cases.
pyo3-stub-gen crate provides the manual way to specify the translation, and pyo3-stub-gen-derive crate provides the default translator as proc-macro based on the mechanism of pyo3-stub-gen.
If you are looking for a working example, please see the examples directory.
| Example | Description |
|---|---|
| examples/pure | Example for Pure Rust maturin project |
| examples/mixed | Example for Mixed Rust/Python maturin project with user-written __init__.py |
| examples/generate_init_py | Example for mixed layout with auto-generated __init__.py (Rust-only workflow) |
Here we describe basic usage of pyo3-stub-gen crate based on examples/pure example.
This crate provides a procedural macro #[gen_stub_pyfunction] and others to generate a Python stub file.
It is used with PyO3's #[pyfunction] macro. Let's consider a simple example PyO3 project:
use pyo3::prelude::*;
#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
Ok((a + b).to_string())
}
#[pymodule]
fn your_module_name(m: &Bound<PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
Ok(())
}
To generate a stub file for this project, please modify it as follows:
use pyo3::prelude::*;
use pyo3_stub_gen::{derive::gen_stub_pyfunction, define_stub_info_gatherer};
#[gen_stub_pyfunction] // Proc-macro attribute to register a function to stub file generator.
#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
Ok((a + b).to_string())
}
#[pymodule]
fn your_module_name(m: &Bound<PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
Ok(())
}
// Define a function to gather stub information.
define_stub_info_gatherer!(stub_info);
[!NOTE] The
#[gen_stub_pyfunction]macro must be placed before#[pyfunction]macro.
#[gen_stub(skip)]For functions or methods that you want to exclude from the generated stub file, use the #[gen_stub(skip)] attribute:
use pyo3::prelude::*;
use pyo3_stub_gen::derive::*;
#[gen_stub_pyclass]
#[pyclass]
struct MyClass;
#[gen_stub_pymethods]
#[pymethods]
impl MyClass {
#[gen_stub(skip)]
fn internal_method(&self) {
// This method will not appear in the .pyi file
}
}
#[gen_stub(default=xx)]For getters, setters, and class attributes, you can specify default values that will appear in the stub file:
use pyo3::prelude::*;
use pyo3_stub_gen::derive::*;
#[gen_stub_pyclass]
#[pyclass]
struct Config {
#[pyo3(get, set)]
#[gen_stub(default = Config::default().timeout)]
timeout: usize,
}
impl Default for Config {
fn default() -> Self {
Config { timeout: 30 }
}
}
#[gen_stub_pymethods]
#[pymethods]
impl Config {
#[getter]
#[gen_stub(default = Config::default().timeout)]
fn get_timeout(&self) -> usize {
self.timeout
}
}
And then, create an executable target in src/bin/stub_gen.rs to generate a stub file:
```rust:ignore use pyo3_stub_gen::Result;
fn main() -> Result<()> {
// stub_info is a function defined by define_stub_info_gatherer! macro.
let stub = pure::stub_info()?;
stub.generate()?;
Ok(())
}
and add `rlib` in addition to `cdylib` in `[lib]` section of `Cargo.toml`:
```toml
[lib]
crate-type = ["cdylib", "rlib"]
This target generates a stub file pure.pyi when executed.
cargo run --bin stub_gen
The stub file is automatically found by maturin, and it is included in the wheel package. See also the maturin document for more details.
In mixed Rust/Python projects, pyo3-stub-gen only generates stub files for PyO3-generated modules (i.e., modules at or below module-name in pyproject.toml). Stub files are not generated for pure Python parent modules to avoid shadowing user's __init__.py files.
If you are upgrading from pyo3-stub-gen v0.18.0–v0.20.0, you may have stale __init__.pyi files in pure Python directories that were previously generated. These stale files should be manually deleted, as type checkers prioritize .pyi files over .py files.
In mixed layout projects, it's common to define PyO3 classes and functions in a hidden internal module (e.g., pkg._core) and re-export them to the public parent module (e.g., pkg). The reexport_module_members! macro declares this re-export relationship:
use pyo3::prelude::*;
use pyo3_stub_gen::{derive::*, reexport_module_members, define_stub_info_gatherer};
#[gen_stub_pyclass]
#[pyclass(module = "pkg._core")]
struct MyClass {
value: i32,
}
#[gen_stub_pyfunction(module = "pkg._core")]
#[pyfunction]
fn my_function() -> i32 { 42 }
#[pymodule]
fn _core(m: &Bound<PyModule>) -> PyResult<()> {
m.add_class::<MyClass>()?;
m.add_function(wrap_pyfunction!(my_function, m)?)?;
Ok(())
}
// Re-export all items from pkg._core to pkg
reexport_module_members!("pkg", "pkg._core");
define_stub_info_gatherer!(stub_info);
This macro:
- Includes re-exported items in the target module's stub file (adds from source_module import ...)
- Provides re-export information to documentation generation
- Serves as the single source of truth for what gets re-exported
The macro can be used between PyO3-generated modules, or from a PyO3 module to a pure Python parent module:
from .submod import .... You must implement the re-export in your #[pymodule] function manually.generate-init-py to be enabled (see below). Otherwise, stub_gen will fail because it cannot generate a stub for a pure Python module without also generating its __init__.py.__init__.pyWhen your project uses mixed layout (required for generating multiple stub files) but you want a Rust-only workflow without writing Python code, the generate-init-py feature automatically generates __init__.py files with proper imports and __all__ declarations based on reexport_module_members! declarations.
Add to pyproject.toml:
[tool.maturin]
module-name = "pkg._core" # PyO3 module is a hidden submodule
python-source = "python"
[tool.pyo3-stub-gen]
generate-init-py = true # Enable for all packages with re-exports
Or specify which packages to generate for:
[tool.pyo3-stub-gen]
generate-init-py = ["pkg", "pkg.subpkg"] # Enable for specific packages
Running cargo run --bin stub_gen generates python/pkg/__init__.py:
# This file is automatically generated by pyo3_stub_gen
# ruff: noqa: F401
from pkg._core import MyClass, my_function
__all__ = [
"MyClass",
"my_function",
]
This ensures consistency between: - Stub files: Type checkers see re-exported items in the parent module - Runtime: Python code can import from the parent module - Documentation: API docs show items under the parent module
See examples/generate_init_py for a complete working example.
When the automatic Rust-to-Python type translation doesn't produce the desired result, you can manually specify type information using Python stub syntax. There are two main approaches:
#[gen_stub_pyfunction(python = "...")]#[gen_stub(override_type(...))]python ParameterUse the python parameter to specify the complete function signature in Python stub syntax. This is ideal when you need to define complex types or when the entire signature needs custom definition.
use pyo3::prelude::*;
use pyo3_stub_gen::derive::*;
#[gen_stub_pyfunction(python = r#"
import collections.abc
import typing
def fn_with_callback(callback: collections.abc.Callable[[str], typing.Any]) -> collections.abc.Callable[[str], typing.Any]:
"""Example using python parameter for complete override."""
"#)]
#[pyfunction]
pub fn fn_with_callback<'a>(callback: Bound<'a, PyAny>) -> PyResult<Bound<'a, PyAny>> {
callback.call1(("Hello!",))?;
Ok(callback)
}
This approach:
- ✅ Provides complete control over the generated stub
- ✅ Supports complex types like collections.abc.Callable
- ✅ Allows adding custom docstrings
- ✅ Import statements are automatically extracted
For selective overrides, use #[gen_stub(override_type(...))] on specific arguments or #[gen_stub(override_return_type(...))] on the function. This is useful when most types translate correctly but a few need adjustment.
use pyo3::prelude::*;
use pyo3_stub_gen::derive::*;
#[gen_stub_pyfunction]
#[pyfunction]
#[gen_stub(override_return_type(type_repr="collections.abc.Callable[[str], typing.Any]", imports=("collections.abc", "typing")))]
pub fn get_callback<'a>(
#[gen_stub(override_type(type_repr="collections.abc.Callable[[str], typing.Any]", imports=("collections.abc", "typing")))]
cb: Bound<'a, PyAny>,
) -> PyResult<Bound<'a, PyAny>> {
Ok(cb)
}
This approach: - ✅ Fine-grained control over individual types - ✅ Preserves automatic generation for other parameters - ✅ Explicit about which types need manual specification
How submit! works:
The #[gen_stub_pyfunction] and #[gen_stub_pyclass] macros automatically generate submit! blocks internally to register type information. You can also manually add submit! blocks to supplement or override this automatic registration.
When multiple type signatures exist for the same function or method, the stub generator automatically generates @overload decorators in the .pyi file. This enables proper type checking for functions that accept multiple type signatures.
Two approaches for overloads:
python_overload pa$ claude mcp add pyo3-stub-gen \
-- python -m otcore.mcp_server <graph>