Oxyde: a Pydantic-centric async ORM with a Rust core
Oxyde ORM is a type-safe, Pydantic-centric asynchronous ORM with a high-performance Rust core designed for clarity, speed, and reliability.
At a glance
- What is it?
- Oxyde gives Python developers a Django-style Model.objects.filter() API backed by Pydantic v2 models, with SQL generation and execution in native Rust and support for PostgreSQL, SQLite and MySQL. It is young and pre-1.0, and the benchmark table in its readme is the vendor's own, so it should be read with care.
- Who is it for?
- Oxyde fits a Python team that wants an async ORM with typed Pydantic models, a familiar Django-style query API and a native core, and it does not fit a team that needs a stable API or a mature ecosystem today, since the project states it is young and that the API may evolve between minor versions.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Django syntax, Pydantic models, Rust underneath
Oxyde is a type-safe, Pydantic-centric asynchronous ORM with a high-performance Rust core. The project says it is inspired by the elegance of Django's ORM, and it focuses on explicitness over magic with predictable behavior and strong typing throughout.
The three design decisions in that sentence are the whole architecture. Django-style API means the query surface is familiar: Model.objects.filter() syntax, and for anyone who has written Django, there is nothing new to learn about the query builder. Pydantic v2 models mean the row type and the validation layer are the same object, with full validation, type hints and serialization. The Rust core means SQL generation and execution happen in native Rust rather than in Python.
The repository layout shows the split clearly. There is a python/ directory for the Python package, a crates/ directory with the Rust workspace, and a Cargo.toml at the root whose members are oxyde-codec, oxyde-sql, oxyde-driver, oxyde-migrate and oxyde-core-py. The name oxyde-core-py marks the Python-facing core crate, and the fact that the codec, the SQL builder, the driver layer and the migration engine are separate crates is what makes the layering possible.
The audience is a FastAPI or async Python developer who has outgrown a synchronous ORM and wants typing that holds from the model definition to the query result.
Installation, project init, and the migration CLI
The package is on PyPI and installs like any other:
pip install oxydeThe first step is a project init command, which creates oxyde_config.py holding your database settings and model paths:
oxyde initThen you write your models as Pydantic models with an explicit table declaration:
# models.py
from oxyde import Model, Field
class User(Model):
id: int | None = Field(default=None, db_pk=True)
name: str
email: str = Field(db_unique=True)
age: int | None = Field(default=None)
class Meta:
is_table = TrueThe table is created through a Django-style migration pair, makemigrations and migrate:
oxyde makemigrations
oxyde migrateThat is a deliberately familiar sequence. The Field options named in the example, db_pk=True and db_unique=True, map the Pydantic model onto the database schema, and the Meta class with is_table = True is what tells the ORM that this class is a table rather than a plain model.
The point of writing the schema down as migrations rather than inferring it at startup is that a schema change becomes a reviewable file in version control, the same argument Django made a decade ago.
The async query surface, in full, on one page
The quick start shows the whole lifecycle in one function, which is the clearest statement of what the library feels like in use:
import asyncio
from oxyde import db
from models import User
async def main():
await db.init(default="sqlite:///app.db")
# Create
user = await User.objects.create(name="Alice", email="[email protected]", age=30)
# Read
users = await User.objects.filter(age__gte=18).all()
user = await User.objects.get(id=1)
# Update
user.age = 31
await user.save()
# Delete
await user.delete()
await db.close()
asyncio.run(main())Every database call is awaited, including the reads, which is the substantive difference from a synchronous ORM and the reason the library exists as an async-first design built for modern async Python with asyncio.
The query syntax is Django's, including the double underscore lookup, so age__gte=18 becomes a greater-than-or-equal comparison. Updates go through a mutated model instance and a save call, and deletes are a method on the instance rather than a queryset operation.
A note on the connection: db.init takes a default connection string, and the async with lifecycle is closed explicitly with db.close(). Nothing here is a global implicit session, which is a deliberate choice in an async world where a shared connection across tasks is a bug waiting to happen.
Transactions with savepoints, and the FastAPI lifespan hook
Transactions are a context manager, with savepoints supported:
from oxyde.db import transaction
async with transaction.atomic():
user = await User.objects.create(name="Alice", email="[email protected]")
await Profile.objects.create(user_id=user.id)
# Auto-commits on success, rolls back on exceptionThe behaviour is the expected one, commit on success, roll back when an exception escapes the block. The savepoint detail matters in a nested case, where an inner transaction can be rolled back without discarding the outer work.
For FastAPI there is a dedicated integration, and the shape of it is the interesting part:
from fastapi import FastAPI
from oxyde import db
app = FastAPI(
lifespan=db.lifespan(
default="postgresql://localhost/mydb",
)
)
@app.get("/users")
async def get_users():
return await User.objects.filter(is_active=True).all()db.lifespan is passed into the app's lifespan parameter, so the connection is opened and closed as part of the application's startup and shutdown rather than by a decorator on every route. That is the correct pattern for an async database library under a server that handles concurrent requests, and it means the model code in your routes stays free of connection management.
The example also shows the practical constraint of the filter API: every query is awaited, so a route that needs several records needs several awaits, and there is no implicit batching in the shown code.
Database support and the connection URL forms
Three backends are supported, and the table states the minimum version and the feature set for each:
| Database | Min Version | Status | Notes | |------------|-------------|--------|-------| | PostgreSQL | 12+ | Full | RETURNING, UPSERT, FOR UPDATE/SHARE, JSON, Arrays | | SQLite | 3.35+ | Full | RETURNING, UPSERT, WAL mode by default | | MySQL | 8.0+ | Full | UPSERT via ON DUPLICATE KEY, FOR UPDATE/SHARE |
All three are marked Full, which is a stronger claim than a typical young project makes. The specific features listed are the ones that differ between backends and that an application is most likely to depend on: PostgreSQL gets RETURNING, JSON columns and array types, SQLite gets RETURNING and WAL mode enabled by default, and MySQL gets its UPSERT expressed as ON DUPLICATE KEY.
The minimum versions are not incidental. SQLite 3.35 is the version that introduced RETURNING, so a project relying on that feature will fail on an older system SQLite even though the ORM itself would install.
The connection URLs follow the SQLAlchemy-style scheme:
postgresql://user:password@localhost:5432/database
sqlite:///path/to/database.db
sqlite:///:memory:
mysql://user:password@localhost:3306/databaseThe Rust crates confirm the driver layer is per-backend, with oxyde-driver being where the differences live, and the topic tags name sqlx, which is the Rust SQL toolkit in the stack.
The benchmark table is the vendor's own, and it deserves scrutiny
The readme publishes a comparison against other Python ORMs, with a table of average ops per second and a note that higher is better. On PostgreSQL, Oxyde is listed at 1,475 against 888 for Tortoise, 932 for Piccolo, 736 for Django, 445 for SQLAlchemy, 431 for SQLModel and 80 for Peewee. On MySQL the figures are 1,239 against 794 for Tortoise and 714 for Django, and on SQLite 2,525 against 1,882 for Tortoise and 1,294 for Django.
Read those numbers as a hypothesis rather than a result. The table is produced by the project, the readme does not describe the workload, the data volume, the connection configuration or the Python and Rust versions involved, and the full report lives on the documentation site rather than in the repository. A benchmark that names six competitors and reports no methodology is a marketing artifact first and a measurement second.
What is verifiable from the repository is structural. The Rust core does the SQL generation and execution rather than Python, and the codec crate handles wire formats separately from the driver layer, so the per-operation cost of building and sending a statement genuinely does not go through the Python interpreter. That is a plausible mechanism for a large gap over SQLAlchemy's Core path, and it is also the reason the same gap should shrink on a workload dominated by Python-level work.
The honest summary is that the ORM is plausibly fast, and that the only way to know for your workload is to read the full benchmark report and then run your own queries against your own schema.
A young project with a Rust workspace and a companion admin panel
The readme opens with a heads-up: Oxyde is a young project under active development, the API may evolve between minor versions, and feedback, bug reports and ideas are welcome through issues.
The release history backs that up. v0.8.0 was released on 2026-08-26, the Rust core tags core-v0.7.1 and core-v0.7.0 came on 2026-08-24 and 2026-08-05, and the last push was on 2026-09-29. The Cargo workspace is at version 0.7.1 while the Python package is at 0.8.0, so the two halves are versioned separately and move at their own pace. The repository is not archived.
The engineering setup is more mature than the version number. The Rust workspace pins rust-version 1.88, uses edition 2021, and configures clippy with pedantic warnings enabled while explicitly allowing nursery as too experimental to gate on, and it documents why several lints are turned off, such as the doc-prose lints and the match arm style the codebase prefers. unsafe_code is set to warn. The Makefile runs pytest for unit, integration and smoke tests separately, cargo test --workspace for the Rust side, and coverage through pytest-cov. Formatting is ruff for Python and cargo fmt for Rust.
There is also a companion project, Oxyde Admin, an auto-generated admin panel with zero boilerplate offering automatic CRUD, search, filters and export, theming, authentication and bulk operations, and it supports FastAPI, Litestar, Sanic, Quart and Falcon. It installs separately:
pip install oxyde-adminThe licence is MIT for both the ORM and the Rust workspace, and the documentation site is at oxyde.fatalyst.dev with a quick start, a user guide and a cheatsheet.
Editorial conclusion
Oxyde fits a Python team that wants an async ORM with typed Pydantic models, a familiar Django-style query API and a native core, and it does not fit a team that needs a stable API or a mature ecosystem today, since the project states it is young and that the API may evolve between minor versions. Before adopting it, read the benchmark report on the documentation site rather than the readme table, run oxyde init and oxyde makemigrations on a scratch project to see what the migration files look like, and confirm your database version meets the minimums listed for your backend.
Frequently asked questions
How do I install and set up Oxyde ORM?
Install the package with pip install oxyde, then run oxyde init to create oxyde_config.py with your database settings and model paths, define your models as Pydantic models with db_pk and db_unique fields and a Meta class with is_table = True, and create the tables with oxyde makemigrations followed by oxyde migrate.
Which databases does Oxyde support?
PostgreSQL 12 and later, SQLite 3.35 and later, and MySQL 8.0 and later are all listed with Full status. The supported feature sets include RETURNING and UPSERT on PostgreSQL and SQLite, JSON and array types on PostgreSQL, and UPSERT via ON DUPLICATE KEY on MySQL.
How do I query models with Oxyde?
The query API is Django-style and async. You await User.objects.create(...), await User.objects.filter(age__gte=18).all(), await User.objects.get(id=1), then await user.save() after mutating the instance and await user.delete() to remove it.
How do transactions work in Oxyde?
You use the transaction.atomic() async context manager from oxyde.db, which auto-commits on success and rolls back when an exception is raised, and it supports savepoints. The readme also documents a db.lifespan hook that can be passed to a FastAPI app so the connection opens and closes with the application lifecycle.
Is Oxyde stable enough to use in production?
The readme states that Oxyde is a young project under active development and that the API may evolve between minor versions, and the project is at version 0.8.0 with a separately versioned Rust core. The published benchmark numbers are the project's own, so the documentation site's full benchmark report is the place to check the methodology before relying on them.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/mr-fatalyst-oxyde)Was this useful?