Distribution
Prerequisites: logferry Walkthrough
Next: Cargo Cheatsheet
Once your PyO3 extension works correctly, this article shows how to build distributable wheels, write type stubs so Python type checkers understand your API, and optionally embed the extension inside a larger Python package.
Building a Single-Platform Wheel
maturin build --release
# dist/logferry-0.1.0-cp311-cp311-linux_x86_64.whl
The wheel filename encodes the Python version and platform (cp311 = CPython 3.11, linux_x86_64). It is a self-contained binary — no Rust toolchain needed on the machine that installs it.
pip install dist/logferry-*.whl
python -c "import logferry; print(logferry.ingest_logs.__doc__)"
Cross-Platform Wheels with GitHub Actions
The standard approach: run maturin build on three OS runners and publish all three wheels to PyPI. The maturin-action handles this in about 10 lines of YAML:
# .github/workflows/publish.yml
name: Publish to PyPI
on:
push:
tags: ['v*']
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: PyO3/maturin-action@v1
with:
command: build
args: --release --out dist
- uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.os }}
path: dist
publish:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: wheels-*
merge-multiple: true
path: dist
- uses: PyO3/maturin-action@v1
with:
command: upload
args: --non-interactive --skip-existing dist/*
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
Embedding Inside a Larger Python Package
If logferry should live inside a bigger package (e.g. mlops_toolkit), use maturin's "mixed" layout:
mlops_toolkit/
├── Cargo.toml
├── pyproject.toml
├── src/
│ └── lib.rs # rename #[pymodule] to `fn _logferry`
└── python/
└── mlops_toolkit/
├── __init__.py
└── py.typed
pyproject.toml:
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[project]
name = "mlops-toolkit"
version = "0.1.0"
[tool.maturin]
python-source = "python"
module-name = "mlops_toolkit._logferry"
Rename the #[pymodule] function in lib.rs to fn _logferry(...), then in python/mlops_toolkit/__init__.py:
from mlops_toolkit._logferry import ingest_logs, validate_line, IngestStats
__all__ = ["ingest_logs", "validate_line", "IngestStats"]
Consumers write from mlops_toolkit import ingest_logs and never see that part of it is Rust.
pip install -e . # editable install (rebuilds Rust on change)
maturin develop # alternative during day-to-day development
Writing Type Stubs for mypy / Pylance
PyO3 does not generate .pyi stubs automatically. Without them, type checkers see your extension as Any. Write a stub file alongside the compiled module:
# logferry.pyi
from typing import Optional
class IngestStats:
total_lines: int
parsed_ok: int
parse_errors: int
validation_errors: int
by_level: dict[str, int]
avg_latency_ms: Optional[float]
sample_errors: list[str]
def ingest_logs(
lines: list[str],
num_threads: int = ...,
) -> IngestStats: ...
def validate_line(line: str) -> bool: ...
Place it in the same directory as logferry.so (or in the package's python/ directory for mixed layouts), and add a py.typed marker file:
touch python/logferry/py.typed
With this in place, Pylance and mypy see full types for IngestStats, ingest_logs, and validate_line.
Publishing to PyPI
# Build optimised wheels for the current platform
maturin build --release
# Publish to PyPI (needs MATURIN_PYPI_TOKEN or a configured ~/.pypirc)
maturin publish
# Publish to TestPyPI first (recommended for first-time publishing)
maturin publish --repository testpypi
pyproject.toml for logferry
The existing pyproject.toml is minimal. A production-ready version:
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[project]
name = "logferry"
version = "0.1.0"
description = "A multi-threaded JSON log ingestor exposed to Python via PyO3"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: System :: Logging",
]
[tool.maturin]
features = ["pyo3/extension-module"]
See Also
- PyO3 Overview —
maturin developfor development builds - Cargo Cheatsheet — all maturin commands
- Troubleshooting — common PyO3 / maturin errors