# How Rust rides inside Python

> A tiny JSON parser in Rust shows where PyO3 ports actually get slow, and it is not the parsing.
- Title: How Rust rides inside Python · Belderbos | Shortify
- Summary: A tiny JSON parser in Rust shows where PyO3 ports actually get slow, and it is not the parsing. When you port Rust into Python, profile the boundary…
- Keywords: rust, python, pyo3, performance, bindings, technology, startups, rides, inside, Belderbos
- Source: Belderbos — https://belderbos.dev/blog/how-libraries-run-rust-inside-python
- Author: Bob Belderbos
- Read time: 2 min
- Topics: rust, python, pyo3, performance, bindings, technology, startups
## You already run Rust
Every Pydantic v2 validation call hands the work to a Rust extension. Its core, pydantic-core, is built with PyO3, the same toolchain used to build anything here.

> Every time you validate data with Pydantic v2 ... a Rust extension does the work.
## A parser small enough to read
The example is a JSON parser written in Rust and exposed to Python so you can import it like any package. The last step, building the Python objects, is the one that matters.
## Four steps to import
Write a normal Rust module. Annotate it with PyO3 macros. Let maturin compile and install it. Then import it.
## Macros are the wiring
#[pyfunction] and #[pymodule] work like Python decorators. They rewrite the function they sit on, adding the glue for calls, type conversion and reference counting at the boundary.

> A Rust attribute macro is close to a Python decorator
## Maturin does the plumbing
Maturin compiles the crate to a shared library.so.dylib or .dll, and drops it in your virtualenv. No packaging steps, import just works.
## Scalars are the easy demo
The earlier tutorial returned a single number. The interesting problems only start once your function returns a structure instead of one value.
## A Rust enum as a tree
The parser returns a plain JsonValue enum: Null, Boolean, Number, String, Array, Object. Each variant carries its own data, so a JSON tree maps onto it cleanly.
## Python never sees it
That tree lives entirely on the Rust side. The PyO3 layer sits on top as a thin adapter, which is why the handoff deserves its own attention.

> That tree lives entirely in Rust. Python never sees it.
## Two lines to expose
parse_json takes a Python token and a &str, returns PyResult<Bound<PyAny>>, and does parse(input)?.into_pyobject(py). Real work on the left, object building on the right.
## Reading the signature
Python<'py> is your access token to the interpreter. Bound<'py, PyAny> is a handle to any Python object. PyResult means value or exception, and ? propagates the failure.
## The return trip costs
into_pyobject walks the whole tree and rebuilds it: a dict per object, a list per array, a float or str per leaf. 100,000 values means 100,000 Python objects, all after parsing finishes.

> this materialization loop, not the parsing, can dominate the end-to-end time
## Don't build what nobody reads
One From impl turns a typed Rust error into a real ValueError with the offset. Then stop materializing trees the caller won't fully touch; hand back a lazy, Rust-backed view instead.

> Getting Rust to run fast is the easy half.
## Key takeaway

When you port Rust into Python, profile the boundary. Building Python objects on the way out can cost more than the algorithm itself.