Show HN: Oxyde – Pydantic-native async ORM with a Rust core (github.com)
If you use FastAPI, you know the drill. You define Pydantic models for your API, then define separate ORM models for your database, then write converters between them. SQLModel tries to fix this but it's still SQLAlchemy underneath. Tortoise gives you a nice Django-style API but its own model system. Django ORM is great but welded to the framework.
I wanted something simple: your Pydantic model IS your database model. One class, full validation on input and output, native type hints, zero duplication. The query API is Django-style (.objects.filter(), .exclude(), Q/F expressions) because I think it's one of the best designs out there.
Explicit over implicit. I tried to remove all the magic. Queries don't touch the database until you call a terminal method like .all(), .get(), or .first(). If you don't explicitly call .join() or .prefetch(), related data won't be loaded. No lazy loading, no surprise N+1 queries behind your back. You see exactly what hits the database by reading the code.
Type safety was a big motivation. Python's weak spot is runtime surprises, so Oxyde tackles this on three levels: (1) when you run makemigrations, it also generates .pyi stub files with fully typed queries, so your IDE knows that filter(age__gte=...) takes an int, that create() accepts exactly the fields your model has, and that .all() returns list[User] not list[Any]; (2) Pydantic validates data going into the database; (3) Pydantic validates data coming back out via model_validate(). You get autocompletion, red squiggles on typos, and runtime guarantees, all from the same model definition.
Why Rust? Not for speed as a goal. I don't do "language X is better" debates. Each one is good at what it was made for. Python is hard to beat for expressing business logic. But infrastructure stuff like SQL generation, connection pooling, and row serialization is where a systems language makes sense. So I split it: Python handles your models and business logic, Rust handles the database plumbing. Queries are built as an IR in Python, serialized via MessagePack, sent to Rust which generates dialect-specific SQL, executes it, and streams results back. Speed is a side effect of this split, not the goal. But since you're not paying a performance tax for the convenience, here are the benchmarks if curious: https://oxyde.fatalyst.dev/latest/advanced/benchmarks/
What's there today: Django-style migrations (makemigrations / migrate), transactions with savepoints, joins and prefetch, PostgreSQL + SQLite + MySQL, FastAPI integration, and an auto-generated admin panel that works with FastAPI, Litestar, Sanic, Quart, and Falcon (https://github.com/mr-fatalyst/oxyde-admin).
It's v0.5, beta, active development, API might still change. This is my attempt to build the ORM I personally wanted to use. Would love feedback, criticism, ideas.
Docs: https://oxyde.fatalyst.dev/
Step-by-step FastAPI tutorial (blog API from scratch): https://github.com/mr-fatalyst/fastapi-oxyde-example
30 comments
[ 6.0 ms ] story [ 59.1 ms ] thread> Why Rust? ... Rust handles the database plumbing. Queries are built as an IR in Python, serialized via MessagePack, sent to Rust which generates dialect-specific SQL, executes it, and streams results back. Speed is a side effect of this split, not the goal.
Nice.
So what does it take to deploy this, dependency wise?
I don’t believe that for a second. Both are wonderful projects, but raw performance was never one of Django ORM’s selling points.
I think its real advantage is making it easy to model new projects and make efficient CRUD calls on those tables. Alchemy’s strong point is “here’s an existing database; let users who grok DB theory query it as efficiently and ergonomically as possible.”
Suppose an API GET /users/:username call returns a "User" type. If returns the the same type as in your database, wouldn't you also get their password hash as well with that request? How do coupled frameworks deal with this?
not really, what makes sense is being JIT-able and friendly to PyPy.
> Type safety was a big motivation.
> https://oxyde.fatalyst.dev/latest/guide/expressions/#basic-u...
> F("views") + 1
If your typed query sub-language can't avoid stringly references to the field names already defined by the schema objects, then it's the lost battle already.
There's already Oxide computers https://oxide.computer/ and Oxc the JS linter/formatter https://oxc.rs/.
https://www.psycopg.org/psycopg3/docs/advanced/typing.html#e...
Django + async is a nightmare so this is a very welcome project.
The auto-generated admin panel is a nice touch. That alone saves days on internal tooling.
2. """You define Pydantic models for your API, then define separate ORM models for your database, then write converters between them.""" So you could've written something that let you convert between two. That would not warrant a whole new ORM.
3. """But infrastructure stuff like SQL generation, connection pooling, and row serialization is where a systems language makes sense."""
This makes no sense to me (except row serialization - maybe, but you're incurring a messagepack overhead instead). Unnecessary native dependency.
With SQLAlchemy approach I can easily find all usages of a particular column everywhere.
Versus Grepping by user_id will obviously produce hundreds and thousands of matches, probably from dozens of tables.Django-ninja is essentially the fastapi of the django world so this library would be enough for me to go all in on fastapi. I just never felt like I found a python orm with the level of ergonomics of django + async of modern python
[1] https://django-ninja.dev/guides/response/django-pydantic/
The big problem with ActiveRecord style ORMs, though, is the big ball of mud you end up with when anything from anywhere in the code can call `save()` on an object at any time to serialise and persist it in the db. It requires a constant vigilance to stop this happening, but many people don't even try in the first place.
What would be ideal is to have an automatically generated DTO-type object that you can pass to other parts of the code that shouldn't be calling `save()` themselves. It could be a "real" object, or just annotated using a Protocol, such that calling `save()` would be a type error. Django models unfortunately don't work well with Prototypes; mypy isn't able to detect the as supporting the Protocol (see: https://github.com/python/mypy/issues/5481). But having an automatically generated DTO could work too.