Why Ferro?¶
The Problem¶
Python ORMs are convenient, but they come with a performance tax. Traditional ORMs like SQLAlchemy, Django ORM, and Tortoise spend significant CPU time in Python code:
- SQL generation — building query strings, escaping values, assembling JOINs
- Row parsing — converting database rows into Python objects
- Object instantiation — calling
__init__, running validators, populating attributes - GIL contention — all of the above happens while holding the Global Interpreter Lock
For simple CRUD this overhead is acceptable. But when you process thousands of rows per request, run high-concurrency workloads, or care about tail latency in services, the Python tax becomes the bottleneck.
How Ferro is Different¶
Ferro moves the expensive parts out of Python and into a Rust engine, connected to Python through a PyO3 FFI bridge.
Rust Core¶
- SQL generation: Sea-Query builds parameterized SQL in Rust
- Row hydration: SQLx executes queries and parses rows GIL-free
- Minimal copying: data flows from database → Rust → Python with zero-copy intent
- Bundled drivers: SQLite and PostgreSQL support is compiled into the engine — no separate driver packages
When you call User.where(lambda t: t.age >= 18).all(), Python only builds a small filter AST. SQL generation, execution, and row parsing all happen in Rust; Python receives hydrated User objects at the end.
Pydantic-Native¶
Unlike ORMs that wrap Pydantic or use it as a serialization layer, Ferro models are Pydantic models:
- Models inherit directly from
pydantic.BaseModel - Validation runs in
pydantic-core(also Rust) - Type hints work exactly as your IDE and type checker expect
- No adapter layer between your ORM models and your API schemas
If you already use FastAPI or any Pydantic-heavy stack, your database models and your validation models are the same objects.
Async-First¶
Ferro is built on sqlx-core and pyo3-async-runtimes:
- True async from Rust to Python — no sync wrappers or thread pools
- Connection pooling handled by SQLx
- Concurrent query execution without blocking the event loop
What You Give Up¶
Ferro is not the right choice for every project. Be honest with yourself about these trade-offs:
- Python 3.13+ only. Ferro targets modern Python and does not support older interpreters.
- Async-only API. There is no synchronous interface. If your application is sync (e.g., classic Flask or scripts without an event loop), Ferro is a poor fit.
- Young feature set. Ferro covers models, queries, mutations, relationships, transactions, and Alembic-based migrations — but some features common in mature ORMs are not implemented yet, including eager loading (
prefetch/select_related), aggregations beyondcount()andexists(), and partial column selects. See the Roadmap for what's planned. - Smaller ecosystem. Fewer third-party integrations, plugins, and Stack Overflow answers than SQLAlchemy or Django.
- Rust at the bottom. You never need Rust to use Ferro, but contributing to or extending the engine requires it, and building from source needs a Rust toolchain.
Comparison¶
| Ferro | SQLAlchemy 2.0 | Django ORM | Tortoise ORM | |
|---|---|---|---|---|
| Core | Rust (SQLx + Sea-Query) | Python | Python | Python |
| Async support | Native, async-only | Native (opt-in) | Limited | Native |
| Type safety | Pydantic models | Typed declarative API | Dynamic | Basic Pydantic integration |
| Learning curve | Low | High | Low | Low |
| Migrations | Alembic (optional extra) | Alembic | Built-in | Aerich |
| Runtime dependencies | Pydantic only | Several | Django | Several |
| Ecosystem maturity | Young | Very mature | Very mature | Moderate |
| Backends | SQLite, PostgreSQL | Many dialects | Many | Several |
Ferro's architecture is designed to make bulk reads, large result sets, and row hydration fast by keeping that work in Rust and outside the GIL. For single-row operations, network and disk latency dominate and every ORM performs similarly — choose based on ergonomics and ecosystem, not microbenchmarks.
When to Choose Ferro¶
Choose Ferro when:
- You're building async services — FastAPI, Starlette, Litestar, or anything on asyncio
- Your codebase is Pydantic-heavy and you want one model class for validation and persistence
- You move lots of rows — data pipelines, bulk ingestion, read-heavy APIs
- You want a small dependency footprint (Pydantic is the only runtime dependency)
- You're on SQLite or PostgreSQL and Python 3.13+
Choose something else when:
- You need a sync API or support for Python < 3.13
- You need dialects beyond SQLite/PostgreSQL (MySQL, MSSQL, Oracle) — use SQLAlchemy
- You're inside a Django project — the integrated Django ORM is the pragmatic choice
- You rely on features Ferro hasn't shipped yet — check the Roadmap before committing
- You need maximum query flexibility for deeply complex SQL — SQLAlchemy Core is hard to beat
Migrating from SQLAlchemy? There's a dedicated guide. Otherwise, the best way to evaluate Ferro is the Quickstart Tutorial — it takes about 10 minutes.