Rylai
Generate Python .pyi stub files from pyo3-annotated Rust source code — statically, without compilation.
Features
- Parses
#[pymodule],#[pyfunction], and#[pyclass]annotations directly from Rust source - Maps Rust types to Python types automatically (
i32→int,Vec<T>→list[T],Option<T>→T | None, etc.) - Extracts doc comments and emits them as Python docstrings
- Generates one
.pyifile per top-level#[pymodule] - Python-version-aware output (
T | Nonefor ≥ 3.10,Optional[T]for older) - Zero-config by default; optionally configured via
rylai.toml
Why Rylai?
Compared with other tools that generate .pyi stubs for PyO3 projects, Rylai offers:
- No compilation — Rylai parses Rust source code directly (via syn). You don’t need to build the crate or depend on compiled artifacts, so stub generation is fast and works even when the project doesn’t compile (e.g. missing native deps or wrong toolchain).
- No code changes — No need to add build scripts,
#[cfg]blocks, or extra annotations to your Rust code. Point Rylai at your crate root and it reads existing#[pymodule]/#[pyfunction]/#[pyclass]as-is. - No Python version lock-in — Stubs are plain text. You generate them once and use them with any Python version; there’s no dependency on a specific Python interpreter or ABI, so you avoid “built for Python 3.x” issues and cross-version workflows stay simple.
Together, this makes Rylai easy to integrate into CI, docs, or local dev without touching your PyO3 code or your Python environment.
Installation
Choose one of the following:
| Method | Command | Notes |
|---|---|---|
| Cargo | cargo install rylai |
Build from source and install to ~/.cargo/bin |
| uv | uv tool install rylai |
Install to uv tools dir; requires publish to PyPI first |
| uvx | uvx rylai |
Run without installing (same as uv; requires PyPI release) |
| crgx | crgx rylai |
Run pre-built binary without compiling; requires crgx and a GitHub Release |
For local development:
Usage
The path you pass is the project root — the folder that contains Cargo.toml (and usually a src/ directory). Rylai scans all .rs files under that project’s src/ and uses the root for rylai.toml, pyproject.toml, etc.
# Run in the current directory (must be the project root with Cargo.toml)
# Specify the project root explicitly (folder containing Cargo.toml)
# Write stubs to a custom output directory
# Use a custom config file
For developers (this repo)
You don’t need to install the binary. Use cargo run and pass arguments after --:
# Generate stubs for the example crate (writes into examples/pyo3_sample/)
# Same as above, with explicit output directory
# Show help
Anything after -- is forwarded to the rylai binary.
Example
Given this Rust source (src/lib.rs):
use *;
Rylai produces pyo3_sample.pyi:
# Auto-generated by rylai. Do not edit manually.
# Module: pyo3_sample
"""Formats the sum of two numbers as string."""
Configuration
You can configure rylai in either (or both) of these places:
rylai.tomlin the crate root[tool.rylai]inpyproject.toml
When both exist, duplicate keys are resolved in favor of rylai.toml; all other options from both files apply. Array tables (e.g. [[override]] / [[tool.rylai.override]]) are replaced as a whole by the same key in rylai.toml, not merged item-by-item. All sections are optional.
Example rylai.toml:
# Root-level keys (e.g. format) should appear before any [section] or [[array]] to avoid being parsed as part of a table.
# After generating .pyi files, run these commands with the generated .pyi paths appended.
# Only use when you trust this config file — commands are executed as configured.
# Each command must be executable (on PATH or use a full path); rylai will error if it cannot be run.
# Empty or whitespace-only entries are ignored.
# You may need "uvx ruff" or "uv/pdm run ruff" instead of "ruff"
= ["ruff format", "ruff check --select I --fix"]
[]
# Target Python version — affects Optional[T] vs T | None syntax (default: "3.10")
= "3.10"
# Prepend auto-generated header comment (default: true)
= true
[]
# What to emit when a type cannot be resolved statically:
# "any" — emit Any and print a warning (default)
# "error" — abort with an error
# "skip" — silently omit the item
= "any"
[]
# cfg features to treat as active during parsing
= ["some_feature"]
[]
# Custom Rust type → Python type overrides
= "numpy.ndarray"
= "numpy.ndarray"
[[]]
# Manually written stub for a specific item (takes precedence over generated output)
= "my_module::complex_function"
= "def complex_function(x: Any, **kwargs: Any) -> dict[str, Any]: ..."
The same options can be set in pyproject.toml under [tool.rylai]:
[]
= "3.10"
[]
= "any"
[]
= "numpy.ndarray"
[[]]
= "my_module::complex_function"
= "def complex_function(x: Any, **kwargs: Any) -> dict[str, Any]: ..."
[]
= ["isort", "black"]
Supported Type Mappings
| Rust type | Python type |
|---|---|
| Scalars | |
i8 … i128, u8 … u128, isize, usize |
int |
f32, f64 |
float |
bool |
bool |
str, String, char |
str |
() |
None |
| Bytes | |
&[u8], [u8] |
bytes |
Vec<u8> |
bytes |
| Path-like | |
Path, PathBuf (incl. std::path::*) |
Path | str / Union[Path, str] |
| Containers | |
Option<T> |
T | None / Optional[T] |
Vec<T> |
list[T] |
(T1, T2, ...) (non-empty tuple) |
tuple[T1, T2, ...] |
HashMap<K,V>, BTreeMap<K,V>, IndexMap<K,V> |
dict[K, V] |
HashSet<T>, BTreeSet<T> |
set[T] |
| PyO3 types | |
PyResult<T>, Result<T, E> |
T (errors become Python exceptions) |
Py<T>, Bound<T>, Borrowed<T> |
recurse into T |
PyRef<T>, PyRefMut<T> |
recurse into T |
PyBytes |
bytes |
PyByteArray |
bytearray |
PyString |
str |
PyDict, PyList, PyTuple, PySet |
dict, list, tuple, set |
PyAny, PyObject |
Any |
| Other | |
Self (in #[pymethods]) |
Self (py ≥ 3.11) or class name |
#[pyclass] structs/enums |
Python class name (from crate) |
| Unknown types | Any (configurable via [fallback]) |
Contributing
Before committing, run the pre-commit checks with prek. See CONTRIBUTING.md for details.