# Build a Python library with a Rust extension

Python is flexible and productive, but sometimes a function needs to run faster than Python allows. Rust gives you that speed with memory safety and no garbage collector. This tutorial walks you through creating a Python package where the performance-critical code lives in Rust, compiled into a native extension module that Python imports like any other module.

You'll use three tools together:

- [uv](https://pydevtools.com/handbook/reference/uv.md) manages the Python project, dependencies, and virtual environment
- [maturin](https://www.maturin.rs/) is the [build backend](https://pydevtools.com/handbook/explanation/what-is-a-build-backend.md) that compiles Rust code into a Python extension module
- [PyO3](https://pyo3.rs/) is the Rust library that bridges Rust and Python, letting you write Rust functions callable from Python

## Prerequisites

- [uv](https://pydevtools.com/handbook/reference/uv.md) installed ([installation guide](https://docs.astral.sh/uv/getting-started/installation/))
- Rust installed via [rustup](https://rustup.rs/)

Verify both are working:

```console
$ uv --version
uv 0.11.6 (Homebrew 2025-04-01)
$ rustc --version
rustc 1.94.1 (e408947bf 2026-03-25)
```

If either command prints "command not found", install the missing tool before continuing. For Rust, run `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` and restart your terminal.

## Creating the Project

Start by creating a Python library project with `uv init --lib`:

```console
$ uv init --lib string_utils
Initialized project `string-utils` at `/path/to/string_utils`
$ cd string_utils
```

This creates a standard Python library layout with a `pyproject.toml`, `README.md`, `.python-version`, and a `src/string_utils/` package. You'll replace that `src/` directory with Rust source code in the next steps.

Delete the generated `src/` directory since you'll replace it with Rust source code:

```bash
rm -rf src
```
```bash
rmdir /s /q src
```
## Configuring the Build System

Replace the contents of [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md) with a configuration that tells [uv](https://pydevtools.com/handbook/reference/uv.md) to use maturin as the build backend:

```toml {filename="pyproject.toml"}
[project]
name = "string-utils"
version = "0.1.0"
description = "A Python library with Rust extensions"
readme = "README.md"
requires-python = ">=3.10"

[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"

[tool.maturin]
features = ["pyo3/extension-module"]
module-name = "string_utils._core"
python-source = "python"
```

Three things to note in `[tool.maturin]`:

- `features = ["pyo3/extension-module"]` enables PyO3's extension module support, required for building importable `.so`/`.pyd` files
- `module-name = "string_utils._core"` tells maturin the compiled Rust code should be importable as `string_utils._core`
- `python-source = "python"` tells maturin to look for Python source files in a `python/` directory rather than `src/`

## Setting Up the Rust Side

Create a `Cargo.toml` in the project root. This is Rust's equivalent of `pyproject.toml`:

```toml {filename="Cargo.toml"}
[package]
name = "string_utils"
version = "0.1.0"
edition = "2021"

[lib]
name = "_core"
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.24", features = ["extension-module"] }
```

The `crate-type = ["cdylib"]` line tells Rust to compile a C-compatible dynamic library, which is what Python loads as an extension module.

Now create the Rust source file:

```bash
mkdir src
```

```rust {filename="src/lib.rs"}
use pyo3::prelude::*;

/// Count the number of words in a string.
#[pyfunction]
fn word_count(text: &str) -> usize {
    text.split_whitespace().count()
}

/// Reverse a string.
#[pyfunction]
fn reverse(text: &str) -> String {
    text.chars().rev().collect()
}

/// Check if a string is a palindrome (ignoring case and whitespace).
#[pyfunction]
fn is_palindrome(text: &str) -> bool {
    let cleaned: String = text.chars().filter(|c| !c.is_whitespace()).collect();
    let lower = cleaned.to_lowercase();
    lower == lower.chars().rev().collect::<String>()
}

/// A Python module implemented in Rust.
#[pymodule]
fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(word_count, m)?)?;
    m.add_function(wrap_pyfunction!(reverse, m)?)?;
    m.add_function(wrap_pyfunction!(is_palindrome, m)?)?;
    Ok(())
}
```

Each `#[pyfunction]` attribute marks a Rust function that PyO3 will expose to Python. The `#[pymodule]` function defines the module itself and registers each function in it.

The module function name `_core` matches the `name = "_core"` in `Cargo.toml` and the `module-name = "string_utils._core"` in `pyproject.toml`. If these three names don't match, the build will succeed but Python will raise `ImportError` when you try to import the module. If you hit an `ImportError`, check that all three files agree on `_core`.

## Adding the Python Wrapper

Create a `python/string_utils/` directory with an `__init__.py` that re-exports the Rust functions:

```bash
mkdir -p python/string_utils
```
```bash
mkdir python\string_utils
```
```python {filename="python/string_utils/__init__.py"}
from string_utils._core import word_count, reverse, is_palindrome

__all__ = ["word_count", "reverse", "is_palindrome"]
```

This gives users a clean import path (`from string_utils import word_count`) while keeping the compiled Rust module as an implementation detail behind `_core`.

## Building and Testing

Run `uv sync` to compile the Rust code and install the package into your virtual environment. If you see `error: failed to run custom build command for 'pyo3-build-config'`, your Rust toolchain may be too old. Run `rustup update` and try again.

```console
$ uv sync
Using CPython 3.12.10
Creating virtual environment at: .venv
Resolved 1 package in 16ms
   Building string-utils @ file:///root/string_utils
Installed 1 package in 21s
 + string-utils==0.1.0
```

The first build takes longer because Cargo downloads and compiles PyO3 and its dependencies. Subsequent builds reuse the cached artifacts in the `target/` directory.

Notice the new `target/` directory and `Cargo.lock` file that appeared in your project root. Rust uses `Cargo.lock` to pin dependency versions, similar to how uv uses `uv.lock` for Python dependencies.

Now test the functions:

```console
$ uv run python -c "
from string_utils import word_count, reverse, is_palindrome

print(word_count('hello world'))
print(reverse('hello'))
print(is_palindrome('racecar'))
print(is_palindrome('Race Car'))
"
2
olleh
True
True
```

`is_palindrome('Race Car')` returns `True` because the Rust implementation strips whitespace and compares case-insensitively. From Python's perspective, these are ordinary functions in an ordinary module. The `from string_utils import word_count` import path works because `__init__.py` re-exports the Rust functions from the compiled `_core` extension.

## Project Structure

Your final project layout looks like this:

{{< /filetree/folder >}}
    {{< /filetree/folder >}}
    {{< /filetree/folder >}}
    {{< /filetree/folder >}}
  {{< /filetree/folder >}}
{{< /filetree/container >}}

- `src/lib.rs` contains the Rust implementation
- `python/string_utils/__init__.py` provides the Python interface
- `Cargo.toml` configures the Rust build
- `pyproject.toml` configures the Python package and tells uv to use maturin
- `target/` holds Rust build artifacts (add to `.gitignore`)

## Next Steps

From here you can:

- Add more complex Rust functions that process large datasets or perform CPU-intensive computation
- Write tests with [pytest](https://pydevtools.com/handbook/tutorial/setting-up-testing-with-pytest-and-uv.md) that exercise both the Rust functions and the Python wrapper
- [Publish the package to PyPI](https://pydevtools.com/handbook/tutorial/publishing-your-first-python-package-to-pypi.md) (maturin handles building platform-specific wheels)

## Learn More

- [Complete example project on GitHub](https://github.com/python-developer-tooling-handbook/python-rust-extension-example)
- [maturin User Guide](https://www.maturin.rs/)
- [PyO3 User Guide](https://pyo3.rs/)
- [What is a build backend?](https://pydevtools.com/handbook/explanation/what-is-a-build-backend.md)
- [uv documentation](https://docs.astral.sh/uv/)
