A Django-inspired ORM for Python with full synchronous and asynchronous support. The same API you know from Django, without depending on the full framework.
Works with SQLite, PostgreSQL, MySQL / MariaDB, libsql / Turso and (4.0+) DuckDB for embedded analytics. Ships with migrations + linter, atomic transactions, signals, validation, relationship loading (select_related / prefetch_related / FilteredRelation), aggregations, DB functions, async-native ORM path, queryset & row caching, audit-trail tracking (@track_history), multi-tenancy (schema-level + row-level), recursive CTEs, full-text search, GIS, and Pydantic interop — all with real static typing (Field[T]).
Production primitives: query budget (HTTP SLA), circuit breaker, outbox pattern, hash sharding, idempotency keys, lag-aware read routing, async pool task affinity, online (zero-downtime) migrations, schema drift detection (dorm diff).
Sibling packages: pytest-djanorm (test fixtures) and djanorm-mypy (mypy plugin) ship in their own packages so the main wheel never pulls dev tooling.
Release notes for every version live in CHANGELOG.md. For the 4.0 highlights see docs/v4_0.md; upgrading from 3.3 → 4.0 is documented in docs/upgrading-to-4.0.md.
# SQLite
pip install "djanorm[sqlite]"
# PostgreSQL
pip install "djanorm[postgresql]"
# libsql / Turso (local, embedded replica or remote)
pip install "djanorm[libsql]"
# DuckDB (embedded analytics, 4.0+)
pip install "djanorm[duckdb]"
# Optional extras
pip install "djanorm[redis]" # queryset + row cache backend
pip install "djanorm[encrypted]" # AES-GCM EncryptedCharField/TextField
pip install "djanorm[pydantic]" # FastAPI-friendly DormSchema
pip install "djanorm[s3]" # FileField on AWS S3 / MinIO / R2 / B2
# Dev tooling (sibling packages)
pip install pytest-djanorm djanorm-mypydorm init blogThat creates:
settings.py— uncomment theDATABASESblock matching your backend.blog/— an app package with an emptymodels.py.
A minimal settings.py looks like:
DATABASES = {
"default": {
"ENGINE": "sqlite",
"NAME": "db.sqlite3",
},
}
INSTALLED_APPS = ["blog"]# blog/models.py
import dorm
class Author(dorm.Model):
name = dorm.CharField(max_length=100)
email = dorm.EmailField(unique=True)
is_active = dorm.BooleanField(default=True)
class Post(dorm.Model):
title = dorm.CharField(max_length=200)
body = dorm.TextField()
author = dorm.ForeignKey(Author, on_delete=dorm.CASCADE)
published_at = dorm.DateTimeField(null=True, blank=True)
class Meta:
ordering = ["-published_at"]dorm makemigrations blog
dorm migrateOpen a shell with dorm shell (IPython auto-detected) or import
the models from your own script.
from blog.models import Author, Post
# Create
alice = Author.objects.create(name="Alice", email="alice@example.com")
post = Post.objects.create(
title="Hello world",
body="First post body.",
author=alice,
)
# Bulk create
Post.objects.bulk_create([
Post(title=f"Draft {i}", body="...", author=alice)
for i in range(5)
])
# Filter / exclude / Q / F
from dorm import Q, F
active_authors = Author.objects.filter(is_active=True)
some_posts = Post.objects.filter(
Q(title__icontains="hello") | Q(title__startswith="Draft")
).exclude(published_at__isnull=True)
# Lookups across relations
alices_posts = Post.objects.filter(author__name="Alice")
# select_related / prefetch_related to dodge N+1
for post in Post.objects.select_related("author"):
print(post.author.name, post.title) # 1 query, JOIN
# Get one
post = Post.objects.get(pk=1)
# Update — single instance
post.title = "Renamed"
post.save()
# Update — bulk via queryset
Post.objects.filter(author=alice).update(title=F("title") + " (by Alice)")
# Delete — single instance
post.delete()
# Delete — bulk
Post.objects.filter(published_at__isnull=True).delete()from blog.models import Author, Post
async def main():
alice = await Author.objects.acreate(name="Alice", email="a@x.com")
post = await Post.objects.acreate(title="Hi", body="...", author=alice)
async for p in Post.objects.filter(author=alice):
print(p.title)
await Post.objects.filter(pk=post.pk).aupdate(title="Hi!")
await post.adelete()from dorm import transaction
with transaction.atomic():
alice = Author.objects.create(name="Alice", email="a@x.com")
Post.objects.create(title="t", body="b", author=alice)
# any exception here rolls back both insertsThe full documentation, tutorials and API reference are published at:
https://rroblf01.github.io/d-orm/
You will find the getting-started guide, complete examples, the API reference and production deployment notes there.
Everyone is welcome to get involved! If you want to suggest changes, propose improvements or discuss the direction of the project, open an issue or a pull request on this repository. Discussions, ideas and critiques are very welcome.
See LICENSE.