| name | setup-maturin-lib |
| description | Sets up a new Rust/Python hybrid library project using maturin and uv. Creates project structure with PyO3 bindings, installs dev dependencies, and optionally configures git remote. Use when the user wants to initialise a Python library with Rust extensions. |
| user-invocable | true |
| allowed-tools | Bash, Write, AskUserQuestion |
Setting Up a Rust/Python Library with maturin
Overview
This skill scaffolds a Rust/Python hybrid library project using maturin and uv in the current working directory. It creates a mixed layout where Rust code lives in src/ and Python code lives in python/.
Workflow
Step 1: Gather Project Details
Ask the user for:
- Python version - default to 3.12 if not specified
- Minimum Python version for abi3 - default to 3.9 (determines wheel compatibility)
- Project description (optional) - for pyproject.toml and README
Verify prerequisites are installed (auto-install maturin if missing):
command -v maturin >/dev/null || pipx install maturin
command -v uv >/dev/null || echo "uv not found"
command -v cargo >/dev/null || echo "cargo not found"
Step 2: Validate Project Name
Get the project name from the current directory and check it's not a reserved Rust keyword:
PROJECT_NAME=$(basename "$(pwd)")
Reserved names that cannot be used: test, main, std, core, self, super, crate, Self, proc_macro
If the directory name is reserved, use AskUserQuestion to prompt for an alternative project name. The alternative name will be used for the package while keeping the directory name unchanged.
Step 3: Initialise with maturin
Use a temp directory to scaffold (maturin refuses to create in existing directories):
TEMP_DIR=$(mktemp -d)
maturin new "$TEMP_DIR/$PROJECT_NAME" --bindings pyo3 --name "$PROJECT_NAME"
cp -r "$TEMP_DIR/$PROJECT_NAME"/. .
rm -rf "$TEMP_DIR"
This creates:
Cargo.toml- Rust project configurationpyproject.toml- Python project configuration with maturin build backendsrc/lib.rs- Initial Rust code with PyO3 bindings
Step 4: Configure pyproject.toml for mixed layout
Update pyproject.toml to support a mixed Python/Rust layout:
[project]
name = "<project-name>"
version = "0.1.0"
description = "<description if provided>"
requires-python = ">= 3.<min-version>"
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[tool.maturin]
python-source = "python"
module-name = "<project_name>._<project_name>"
features = ["pyo3/extension-module"]
The module-name setting puts the Rust extension at <package>._<package> so it doesn't conflict with the Python package.
Step 5: Configure Cargo.toml
Update Cargo.toml with proper abi3 configuration:
[package]
name = "<project-name>"
version = "0.1.0"
edition = "2021"
[lib]
name = "_<project_name>"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.23", features = ["abi3-py3<min-version>", "extension-module"] }
The underscore prefix on the lib name matches the module-name in pyproject.toml.
Step 6: Create mixed Python/Rust layout
Create the Python package that wraps the Rust extension:
PROJECT_NAME=$(basename "$(pwd)")
PACKAGE_NAME=$(echo "$PROJECT_NAME" | tr '-' '_')
mkdir -p "python/$PACKAGE_NAME"
Create python/<package>/__init__.py:
"""<project-name> - A Rust/Python hybrid library."""
from ._<package_name> import *
__all__ = [] # Populate with exported names from Rust
Update src/lib.rs with the matching module name:
use pyo3::prelude::*;
/// A simple example function.
#[pyfunction]
fn hello() -> String {
"Hello from Rust!".to_string()
}
/// The Python module definition.
#[pymodule]
fn _<package_name>(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(hello, m)?)?;
Ok(())
}
Step 7: Add dev dependencies
Note: Do NOT run uv init - maturin already created pyproject.toml. Just use uv add directly:
uv add --dev maturin ipython jupyterlab ruff pytest pytest-cov
Adding maturin as a dev dependency allows uv run maturin develop.
Step 8: Create project structure
Create directories:
mkdir -p tests
mkdir -p nbs
mkdir -p scratch/scripts
mkdir -p scratch/nbs
touch tests/__init__.py
tests/- test filesnbs/- notebooks (tracked in git)scratch/scripts/- throwaway scripts (ignored)scratch/nbs/- throwaway notebooks (ignored)
Add to .gitignore:
cat >> .gitignore << 'EOF'
scratch/
target/
*.so
*.pyd
EOF
Create a minimal test file tests/test_placeholder.py:
def test_placeholder():
"""Placeholder test to verify pytest works."""
assert True
Step 9: Create README.md
Create a minimal README using the directory name as the project name:
# <directory-name>
<description if provided>
A Rust/Python hybrid library using maturin and PyO3.
## Development
```bash
just dev # Build and install the extension
just test # Run tests
just check # Format and lint
### Step 10: Create CLAUDE.md
Create a `CLAUDE.md` with project context:
```markdown
# <project-name>
Rust/Python hybrid library using maturin for building and uv for Python dependency management.
## Commands
Run `just` to see available commands.
Key commands:
- `just dev` - Build and install the Rust extension for development
- `just test` - Run tests (runs `dev` first)
- `just check` - Format and lint both Python and Rust code
## Project Structure
- `src/` - Rust source code (the extension module)
- `python/<package>/` - Python source code (wraps Rust extension)
- `tests/` - test files
- `nbs/` - notebooks (tracked)
- `scratch/` - throwaway scripts and notebooks (gitignored)
## Dependencies
- Add Python deps: `uv add <package>`
- Add dev deps: `uv add --dev <package>`
- Add Rust deps: Edit `Cargo.toml`
## Workflow
1. Edit Rust code in `src/lib.rs`
2. Run `just dev` to rebuild the extension
3. Import in Python: `from <package> import hello`
The Rust extension is built as `<package>._<package>` and re-exported from `<package>/__init__.py`.
Step 11: Create justfile
Create a justfile with development commands:
# List available commands
default:
@just --list
# Build and install the Rust extension for development
dev:
uv run maturin develop --uv
# Build in release mode
dev-release:
uv run maturin develop --uv --release
# Run tests (builds first)
test *args: dev
uv run pytest {{args}}
# Run tests with coverage
test-cov: dev
uv run pytest --cov
# Format Python code
fmt-py:
uv run ruff format .
# Format Rust code
fmt-rs:
cargo fmt
# Format all code
fmt: fmt-py fmt-rs
# Lint Python code
lint-py:
uv run ruff check .
# Lint Rust code
lint-rs:
cargo clippy --all-targets -- -D warnings
# Lint all code
lint: lint-py lint-rs
# Lint and fix Python
lint-fix:
uv run ruff check . --fix
# Format and lint all
check: fmt lint
# Start IPython shell with autoreload (builds first)
shell: dev
uv run ipython -i -c "get_ipython().run_line_magic('load_ext', 'autoreload'); get_ipython().run_line_magic('autoreload', '2')"
# Start Jupyter Lab
jupyter:
uv run jupyter lab
# Build wheel
build:
uv run maturin build --release
Step 12: Git Repository Setup
First, discover available GitHub organisations:
gh org list
Then ask the user using AskUserQuestion with these options:
- No remote - Keep it local only
- Public repository (personal) - Create public repo in personal account
- Private repository (personal) - Create private repo in personal account
- Organisation repository - Show list of orgs from
gh org listand let user pick, then ask public/private
If user selects organisation, present the orgs discovered and ask which one, then ask public or private.
Creating the repository:
For personal repos:
gh repo create <repo-name> --public|--private --source . --push
For organisation repos:
gh repo create <org-name>/<repo-name> --public|--private --source . --push
If no remote wanted:
git init
git add .
git commit -m "Initial commit: scaffold Rust/Python library with maturin"
Step 13: Verify Build
Build the extension and verify it works by calling the hello function from Python:
just dev
Then test the import:
PACKAGE_NAME=$(basename "$(pwd)" | tr '-' '_')
uv run python -c "from $PACKAGE_NAME import hello; print(hello())"
Expected output: Hello from Rust!
If the build or import fails, debug the issue before completing. Common problems:
- Module name mismatch between Cargo.toml
lib.nameand pyproject.tomlmodule-name - Missing PyO3 features in Cargo.toml
- Python import path issues in
__init__.py
Checklist
- Confirm Python version (default 3.12)
- Confirm minimum Python version for abi3 (default 3.9)
- Get optional project description
- Verify maturin, uv, and cargo are installed (auto-install maturin via pipx if missing)
- Validate project name is not a reserved Rust keyword
- Run
maturin newvia temp directory workaround - Configure pyproject.toml for mixed layout
- Configure Cargo.toml with abi3 features
- Create Python package in
python/<package>/ - Update src/lib.rs with matching module name
- Add dev dependencies (maturin, ipython, jupyterlab, ruff, pytest, pytest-cov)
- Create directories (tests/, nbs/, scratch/scripts/, scratch/nbs/)
- Add scratch/, target/, *.so, *.pyd to .gitignore
- Create placeholder test
- Create README.md
- Create CLAUDE.md
- Create justfile with dev commands
- Fetch available GitHub orgs with
gh org list - Ask about git remote setup
- Initialise git and optionally create remote
- Build extension with
just devand verify hello function works from Python
Notes
- Uses
maturin newfor scaffolding to ensure correct PyO3 setup - Mixed layout puts Python in
python/and Rust insrc/ - The Rust module is prefixed with underscore (
_<package>) to distinguish from Python package - abi3 feature creates wheels compatible across Python versions
just devmust be run before tests or shell to build the extension- Verify maturin, uv, cargo, and gh are installed before starting