From 7517d934a639c3cc96c73514c697e91aa724e8ea Mon Sep 17 00:00:00 2001 From: person93 Date: Sat, 19 Sep 2026 00:17:05 -0400 Subject: [PATCH 1/8] add example `maturin-starter-nostd` --- .../.template/Cargo.toml | 16 ++++++++ .../.template/pre-script.rhai | 4 ++ .../.template/pyproject.toml | 10 +++++ examples/maturin-starter-nostd/Cargo.toml | 21 +++++++++++ examples/maturin-starter-nostd/MANIFEST.in | 2 + examples/maturin-starter-nostd/README.md | 35 ++++++++++++++++++ .../maturin-starter-nostd/cargo-generate.toml | 5 +++ .../maturin_starter/__init__.py | 15 ++++++++ examples/maturin-starter-nostd/noxfile.py | 17 +++++++++ examples/maturin-starter-nostd/pyproject.toml | 19 ++++++++++ examples/maturin-starter-nostd/src/lib.rs | 37 +++++++++++++++++++ .../maturin-starter-nostd/src/submodule.rs | 22 +++++++++++ .../tests/test_maturin_starter.py | 19 ++++++++++ .../tests/test_submodule.py | 6 +++ 14 files changed, 228 insertions(+) create mode 100644 examples/maturin-starter-nostd/.template/Cargo.toml create mode 100644 examples/maturin-starter-nostd/.template/pre-script.rhai create mode 100644 examples/maturin-starter-nostd/.template/pyproject.toml create mode 100644 examples/maturin-starter-nostd/Cargo.toml create mode 100644 examples/maturin-starter-nostd/MANIFEST.in create mode 100644 examples/maturin-starter-nostd/README.md create mode 100644 examples/maturin-starter-nostd/cargo-generate.toml create mode 100644 examples/maturin-starter-nostd/maturin_starter/__init__.py create mode 100644 examples/maturin-starter-nostd/noxfile.py create mode 100644 examples/maturin-starter-nostd/pyproject.toml create mode 100644 examples/maturin-starter-nostd/src/lib.rs create mode 100644 examples/maturin-starter-nostd/src/submodule.rs create mode 100644 examples/maturin-starter-nostd/tests/test_maturin_starter.py create mode 100644 examples/maturin-starter-nostd/tests/test_submodule.py diff --git a/examples/maturin-starter-nostd/.template/Cargo.toml b/examples/maturin-starter-nostd/.template/Cargo.toml new file mode 100644 index 00000000000..dfd6cc14de8 --- /dev/null +++ b/examples/maturin-starter-nostd/.template/Cargo.toml @@ -0,0 +1,16 @@ +[package] +authors = ["{{authors}}"] +name = "{{project-name}}" +version = "0.1.0" +edition = "2021" + +[lib] +name = "maturin_starter" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "{{PYO3_VERSION}}", default-features = false, features = [ + "macros", + "hashbrown", + "parking_lot", +] } diff --git a/examples/maturin-starter-nostd/.template/pre-script.rhai b/examples/maturin-starter-nostd/.template/pre-script.rhai new file mode 100644 index 00000000000..76ae8c8c984 --- /dev/null +++ b/examples/maturin-starter-nostd/.template/pre-script.rhai @@ -0,0 +1,4 @@ +variable::set("PYO3_VERSION", "0.29.2"); +file::rename(".template/Cargo.toml", "Cargo.toml"); +file::rename(".template/pyproject.toml", "pyproject.toml"); +file::delete(".template"); diff --git a/examples/maturin-starter-nostd/.template/pyproject.toml b/examples/maturin-starter-nostd/.template/pyproject.toml new file mode 100644 index 00000000000..d27a1be9832 --- /dev/null +++ b/examples/maturin-starter-nostd/.template/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = ["maturin>=1,<2"] +build-backend = "maturin" + +[project] +name = "{{project-name}}" +version = "0.1.0" + +[project.optional-dependencies] +dev = ["pytest"] diff --git a/examples/maturin-starter-nostd/Cargo.toml b/examples/maturin-starter-nostd/Cargo.toml new file mode 100644 index 00000000000..c34a16b3c72 --- /dev/null +++ b/examples/maturin-starter-nostd/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "maturin-starter" +version = "0.1.0" +edition = "2021" +rust-version = "1.83" + +[lib] +name = "maturin_starter" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { path = "../../", default-features = false, features = [ + "macros", + "hashbrown", + "parking_lot", +] } + +[features] +abi3 = ["pyo3/abi3-py39"] + +[workspace] diff --git a/examples/maturin-starter-nostd/MANIFEST.in b/examples/maturin-starter-nostd/MANIFEST.in new file mode 100644 index 00000000000..becccf7bc9a --- /dev/null +++ b/examples/maturin-starter-nostd/MANIFEST.in @@ -0,0 +1,2 @@ +include pyproject.toml Cargo.toml +recursive-include src * diff --git a/examples/maturin-starter-nostd/README.md b/examples/maturin-starter-nostd/README.md new file mode 100644 index 00000000000..da1ee9e4a41 --- /dev/null +++ b/examples/maturin-starter-nostd/README.md @@ -0,0 +1,35 @@ +# maturin-starter + +An example of a basic Python extension module built using PyO3 and [`maturin`](https://github.com/PyO3/maturin). + +## Building and Testing + +To build this package, first install `maturin`: + +```shell +pip install maturin +``` + +To build and test use `maturin develop`: + +```shell +pip install -r requirements-dev.txt +maturin develop && pytest +``` + +Alternatively, install nox and run the tests inside an isolated environment: + +```shell +nox +``` + +## Copying this example + +Use [`cargo-generate`](https://crates.io/crates/cargo-generate): + +```bash +$ cargo install cargo-generate +$ cargo generate --git https://github.com/PyO3/pyo3 examples/maturin-starter +``` + +(`cargo generate` will take a little while to clone the PyO3 repo first; be patient when waiting for the command to run.) diff --git a/examples/maturin-starter-nostd/cargo-generate.toml b/examples/maturin-starter-nostd/cargo-generate.toml new file mode 100644 index 00000000000..d750c4de7a3 --- /dev/null +++ b/examples/maturin-starter-nostd/cargo-generate.toml @@ -0,0 +1,5 @@ +[template] +ignore = [".nox"] + +[hooks] +pre = [".template/pre-script.rhai"] diff --git a/examples/maturin-starter-nostd/maturin_starter/__init__.py b/examples/maturin-starter-nostd/maturin_starter/__init__.py new file mode 100644 index 00000000000..ab2c15bc559 --- /dev/null +++ b/examples/maturin-starter-nostd/maturin_starter/__init__.py @@ -0,0 +1,15 @@ +# import the contents of the Rust library into the Python extension +from .maturin_starter import * + +# optional: include the documentation from the Rust module +from .maturin_starter import ( + __all__, + __doc__, # noqa: F401 +) + +__all__ = __all__ + ["PythonClass"] + + +class PythonClass: + def __init__(self, value: int) -> None: + self.value = value diff --git a/examples/maturin-starter-nostd/noxfile.py b/examples/maturin-starter-nostd/noxfile.py new file mode 100644 index 00000000000..e5ee4a1a468 --- /dev/null +++ b/examples/maturin-starter-nostd/noxfile.py @@ -0,0 +1,17 @@ +import argparse + +import nox + + +@nox.session +def python(session): + parser = argparse.ArgumentParser() + parser.add_argument("--features") + args = parser.parse_args(session.posargs) + + session.env["MATURIN_PEP517_ARGS"] = "--profile=dev" + if args.features: + session.env["MATURIN_PEP517_ARGS"] += f" --features={args.features}" + + session.install(".[dev]") + session.run("pytest") diff --git a/examples/maturin-starter-nostd/pyproject.toml b/examples/maturin-starter-nostd/pyproject.toml new file mode 100644 index 00000000000..52ecba15edc --- /dev/null +++ b/examples/maturin-starter-nostd/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = ["maturin>=1,<2"] +build-backend = "maturin" + +[project] +name = "maturin-starter" +version = "0.1.0" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Rust", + "Operating System :: POSIX", + "Operating System :: MacOS :: MacOS X", +] + +[project.optional-dependencies] +dev = ["pytest"] diff --git a/examples/maturin-starter-nostd/src/lib.rs b/examples/maturin-starter-nostd/src/lib.rs new file mode 100644 index 00000000000..0bfa4fa3f00 --- /dev/null +++ b/examples/maturin-starter-nostd/src/lib.rs @@ -0,0 +1,37 @@ +#![no_std] + +use pyo3::prelude::*; +use pyo3::types::PyDict; +use pyo3::wrap_pymodule; + +mod submodule; + +#[pyclass] +struct ExampleClass { + #[pyo3(get, set)] + value: i32, +} + +#[pymethods] +impl ExampleClass { + #[new] + pub fn new(value: i32) -> Self { + ExampleClass { value } + } +} + +/// An example module implemented in Rust using PyO3. +#[pymodule] +fn maturin_starter(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_wrapped(wrap_pymodule!(submodule::submodule))?; + + // Inserting to sys.modules allows importing submodules nicely from Python + // e.g. from maturin_starter.submodule import SubmoduleClass + + let sys = PyModule::import(py, "sys")?; + let sys_modules: Bound<'_, PyDict> = sys.getattr("modules")?.cast_into()?; + sys_modules.set_item("maturin_starter.submodule", m.getattr("submodule")?)?; + + Ok(()) +} diff --git a/examples/maturin-starter-nostd/src/submodule.rs b/examples/maturin-starter-nostd/src/submodule.rs new file mode 100644 index 00000000000..f3eb174100b --- /dev/null +++ b/examples/maturin-starter-nostd/src/submodule.rs @@ -0,0 +1,22 @@ +use pyo3::prelude::*; + +#[pyclass] +struct SubmoduleClass {} + +#[pymethods] +impl SubmoduleClass { + #[new] + pub fn __new__() -> Self { + SubmoduleClass {} + } + + pub fn greeting(&self) -> &'static str { + "Hello, world!" + } +} + +#[pymodule] +pub fn submodule(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} diff --git a/examples/maturin-starter-nostd/tests/test_maturin_starter.py b/examples/maturin-starter-nostd/tests/test_maturin_starter.py new file mode 100644 index 00000000000..1f3e8d9f7f4 --- /dev/null +++ b/examples/maturin-starter-nostd/tests/test_maturin_starter.py @@ -0,0 +1,19 @@ +from maturin_starter import ExampleClass, PythonClass + + +def test_python_class() -> None: + py_class = PythonClass(value=10) + assert py_class.value == 10 + + +def test_example_class() -> None: + example = ExampleClass(value=11) + assert example.value == 11 + + +def test_doc() -> None: + import maturin_starter + + assert ( + maturin_starter.__doc__ == "An example module implemented in Rust using PyO3." + ) diff --git a/examples/maturin-starter-nostd/tests/test_submodule.py b/examples/maturin-starter-nostd/tests/test_submodule.py new file mode 100644 index 00000000000..5cf2f57c2cb --- /dev/null +++ b/examples/maturin-starter-nostd/tests/test_submodule.py @@ -0,0 +1,6 @@ +from maturin_starter.submodule import SubmoduleClass + + +def test_submodule_class() -> None: + submodule_class = SubmoduleClass() + assert submodule_class.greeting() == "Hello, world!" From 842d29cb2341d1fe28898a4de3744fc0360f06ec Mon Sep 17 00:00:00 2001 From: person93 Date: Sat, 19 Sep 2026 01:02:01 -0400 Subject: [PATCH 2/8] add tool to verify that no_std build contains no smybols from std --- pyo3-verify-nostd/Cargo.toml | 11 +++++++++++ pyo3-verify-nostd/src/main.rs | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 pyo3-verify-nostd/Cargo.toml create mode 100644 pyo3-verify-nostd/src/main.rs diff --git a/pyo3-verify-nostd/Cargo.toml b/pyo3-verify-nostd/Cargo.toml new file mode 100644 index 00000000000..ce9b38a4348 --- /dev/null +++ b/pyo3-verify-nostd/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "pyo3-verify-nostd" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +anyhow = "1.0.104" +object = "0.40" + +[workspace] diff --git a/pyo3-verify-nostd/src/main.rs b/pyo3-verify-nostd/src/main.rs new file mode 100644 index 00000000000..74380833407 --- /dev/null +++ b/pyo3-verify-nostd/src/main.rs @@ -0,0 +1,25 @@ +use std::{env, fs}; + +use anyhow::{Context, Result, bail, ensure}; +use object::{Object, ObjectSymbol}; + +fn main() -> Result<()> { + const HELP: &str = + "should be called with exactly one command line arg that is path of file to analyze"; + + let mut args = env::args().skip(1); + let path = args.next().context(HELP)?; + ensure!(args.next().is_none(), "{HELP}"); + + let file = fs::read(&path).with_context(|| format!("failed to read file: {path}"))?; + let file = object::read::File::parse(file.as_slice()) + .with_context(|| format!("failed to parse file: {path}"))?; + for symbol in file.symbols() { + let name = symbol.name().context("failed to get name of a symbol")?; + if name.contains("std") { + bail!("found symbol from `std`: {name}"); + } + } + + Ok(()) +} From 8cf67f83c3cee464912dc862be24d0fb2b319f74 Mon Sep 17 00:00:00 2001 From: person93 Date: Sat, 19 Sep 2026 15:48:02 -0400 Subject: [PATCH 3/8] add noxfile session to verify nostd examples doesn't use std --- noxfile.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/noxfile.py b/noxfile.py index 90528b85e87..669a2b1caca 100644 --- a/noxfile.py +++ b/noxfile.py @@ -15,6 +15,7 @@ from difflib import unified_diff from functools import lru_cache from glob import glob +from operator import contains from pathlib import Path from shlex import quote from typing import Any, Literal, Protocol @@ -298,6 +299,83 @@ def clippy(session: nox.Session) -> bool: session.error("one or more jobs failed") +@nox.session(name="verify-nostd", venv_backend="none") +def verify_nostd(session: nox.Sesstion): + manifest_path = "examples/maturin-starter-nostd/Cargo.toml" + args = [ + "cargo", + "build", + f"--manifest-path={manifest_path}", + "--message-format=json-diagnostic-rendered-ansi", + ] + + dylib_path: str | None = None + is_github_actions = _is_github_actions() + failed = False + if is_github_actions: + # Insert ::group:: at the start of nox's command line output + print("::group::", end="", flush=True, file=sys.stderr) + try: + env = os.environ.copy() + env["PYO3_WIP_NO_STD"] = "1" + stdout = subprocess.Popen( + args, + stdout=subprocess.PIPE, + env=env, + ).stdout + assert stdout is not None + for line in io.TextIOWrapper(stdout).readlines(): + line = line.strip() + if not line.startswith("{"): + print(line) + continue + + try: + message = json.loads(line) + reason: str = message["reason"] + + if reason == "compiler-message": + print(message["message"]["rendered"]) + + elif reason == "compiler-artifact": + this_manifest_path: str = message["manifest_path"] + if not this_manifest_path.endswith(manifest_path): + continue + target_kinds: list[str] = message["target"]["kind"] + if not contains(target_kinds, "cdylib"): + continue + filenames: list[str] = message["filenames"] + if len(filenames) != 1: + session.error( + f"found multiple files for cdylib artifact: {filenames}", + ) + return + dylib_path = filenames[0] + + except json.JSONDecodeError: + print(line) + + except Exception: + failed = True + raise + finally: + if is_github_actions: + print("::endgroup::", file=sys.stderr) + # Defer the error message until after the group to make them easier + # to find in the log + if failed: + command = " ".join(args) + print(f"::error::`{command}` failed", file=sys.stderr) + + if dylib_path is None: + session.error("could not find path to compiled dylib artifact") + return + + _run_cargo( + session, "run", "--manifest-path=pyo3-verify-nostd/Cargo.toml", "--", dylib_path + ) + + def _clippy( session: nox.Session, *, From 4c7de49b1914680e263bb24ef89eb0e7bc4348ef Mon Sep 17 00:00:00 2001 From: person93 Date: Fri, 16 Oct 2026 10:58:19 -0400 Subject: [PATCH 4/8] fix maturin starter README --- examples/maturin-starter-nostd/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/maturin-starter-nostd/README.md b/examples/maturin-starter-nostd/README.md index da1ee9e4a41..c3ca80901e6 100644 --- a/examples/maturin-starter-nostd/README.md +++ b/examples/maturin-starter-nostd/README.md @@ -29,7 +29,7 @@ Use [`cargo-generate`](https://crates.io/crates/cargo-generate): ```bash $ cargo install cargo-generate -$ cargo generate --git https://github.com/PyO3/pyo3 examples/maturin-starter +$ cargo generate --git https://github.com/PyO3/pyo3 examples/maturin-starter-nostd ``` (`cargo generate` will take a little while to clone the PyO3 repo first; be patient when waiting for the command to run.) From 40f42195c4c7dd3df1f7b26cca813e584db1278d Mon Sep 17 00:00:00 2001 From: person93 Date: Fri, 16 Oct 2026 11:10:23 -0400 Subject: [PATCH 5/8] verify-nostd lists all symbols it finds Previously, it'd bail at the first one it found. --- pyo3-verify-nostd/src/main.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pyo3-verify-nostd/src/main.rs b/pyo3-verify-nostd/src/main.rs index 74380833407..dbd1367f46c 100644 --- a/pyo3-verify-nostd/src/main.rs +++ b/pyo3-verify-nostd/src/main.rs @@ -1,6 +1,6 @@ use std::{env, fs}; -use anyhow::{Context, Result, bail, ensure}; +use anyhow::{bail, ensure, Context, Result}; use object::{Object, ObjectSymbol}; fn main() -> Result<()> { @@ -14,12 +14,18 @@ fn main() -> Result<()> { let file = fs::read(&path).with_context(|| format!("failed to read file: {path}"))?; let file = object::read::File::parse(file.as_slice()) .with_context(|| format!("failed to parse file: {path}"))?; + let mut count = 0; for symbol in file.symbols() { let name = symbol.name().context("failed to get name of a symbol")?; if name.contains("std") { - bail!("found symbol from `std`: {name}"); + count += 1; + println!("found symbol from `std`: {name}"); } } + if count > 0 { + bail!("\ncompiled library contains {count} symbols from std"); + } + Ok(()) } From 2f4905acdd00a70a372a76792843ae523a73dc9d Mon Sep 17 00:00:00 2001 From: person93 Date: Fri, 16 Oct 2026 11:18:49 -0400 Subject: [PATCH 6/8] add missing import to noxfile --- noxfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/noxfile.py b/noxfile.py index 669a2b1caca..8154c9dd758 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,6 +1,7 @@ from __future__ import annotations import functools +import io import json import os import re From 6de95f74ff31e189c89d8542d5a51371f956a3ed Mon Sep 17 00:00:00 2001 From: person93 Date: Fri, 16 Oct 2026 11:20:11 -0400 Subject: [PATCH 7/8] add ci job to run the nostd verifier --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02967ea262b..b52ae874895 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -917,6 +917,26 @@ jobs: - run: uv run --project .. --isolated --only-group nox nox -s ${{matrix.checker}} working-directory: pytests + verify-nostd: + needs: [fmt] + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install Rust toolchain + run: | + rustup toolchain install stable --profile minimal --no-self-update --component rust-src + rustup default stable + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + save-cache: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} + - run: uv run --isolated --only-group nox nox -s verify-nostd + conclusion: needs: - zizmor From f83eb648e90a95cedaecca5c97b73f02937a165c Mon Sep 17 00:00:00 2001 From: person93 Date: Tue, 27 Oct 2026 06:13:28 -0400 Subject: [PATCH 8/8] ignore false positives from conversions::std module --- pyo3-verify-nostd/src/main.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pyo3-verify-nostd/src/main.rs b/pyo3-verify-nostd/src/main.rs index dbd1367f46c..3ebc03c258e 100644 --- a/pyo3-verify-nostd/src/main.rs +++ b/pyo3-verify-nostd/src/main.rs @@ -1,6 +1,6 @@ use std::{env, fs}; -use anyhow::{bail, ensure, Context, Result}; +use anyhow::{Context, Result, bail, ensure}; use object::{Object, ObjectSymbol}; fn main() -> Result<()> { @@ -17,7 +17,7 @@ fn main() -> Result<()> { let mut count = 0; for symbol in file.symbols() { let name = symbol.name().context("failed to get name of a symbol")?; - if name.contains("std") { + if is_std_symbol(name) { count += 1; println!("found symbol from `std`: {name}"); } @@ -29,3 +29,14 @@ fn main() -> Result<()> { Ok(()) } + +fn is_std_symbol(name: &str) -> bool { + let Some(idx_std) = name.find("std") else { + return false; + }; + // ignore false positives from the conversions::std module + match name.find("conversions") { + Some(idx_conversions) => idx_conversions > idx_std, + None => true, + } +}