Bridge Django ORM models to Tortoise ORM for truly async database access. Define your models once in Django, query them asynchronously via Tortoise — no duplicate schema, no manual sync.
Why?
Django's async ORM wraps synchronous calls in sync_to_async threads. Tortoise ORM is natively async but requires its own model definitions. This library eliminates that trade-off:
- Single source of truth — Django models define the schema, migrations, and admin
- Truly async queries — Tortoise ORM handles the actual database I/O with native async drivers
- Zero boilerplate — Add to
INSTALLED_APPS, and every model gets atortoise_objectsmanager
Installation
pip install django-tortoise-objects
With database drivers:
# PostgreSQL pip install django-tortoise-objects[pg] # SQLite pip install django-tortoise-objects[sqlite] # MySQL pip install django-tortoise-objects[mysql] # All drivers pip install django-tortoise-objects[all]
Quick Start
1. Add to INSTALLED_APPS
INSTALLED_APPS = [ # ... "django_tortoise", # ... your apps ]
2. Query your existing Django models asynchronously
from myapp.models import Article # All standard query operations — fully async, no sync_to_async wrapper articles = await Article.tortoise_objects.filter(published=True) article = await Article.tortoise_objects.get(id=42) count = await Article.tortoise_objects.count() exists = await Article.tortoise_objects.filter(title="foo").exists() # Create, update, delete article = await Article.tortoise_objects.create(title="Hello", body="World") await Article.tortoise_objects.filter(published=False).update(draft=True) await Article.tortoise_objects.filter(id=42).delete() # Chaining recent = await Article.tortoise_objects.filter(published=True).order_by("-created_at").limit(10) # Prefetch relations (via underlying Tortoise model) employees = await Employee.tortoise_objects.model.all().prefetch_related("team")
3. Use in async Django views
from django.http import JsonResponse from myapp.models import Tag async def tag_list(request): tags = await Tag.tortoise_objects.all() return JsonResponse({"tags": [{"id": t.id, "name": t.name} for t in tags]})
That's it. No Tortoise model definitions, no configuration files, no manual initialization.
Configuration
Optional settings via TORTOISE_OBJECTS in your Django settings:
TORTOISE_OBJECTS = { # Include only specific app models (fnmatch patterns) "INCLUDE_MODELS": ["myapp.*", "blog.*"], # Exclude specific models "EXCLUDE_MODELS": ["auth.*", "admin.*"], # Override database backend mapping "DB_ENGINE_MAP": { "django.db.backends.postgresql": "tortoise.backends.psycopg", }, # Connection pool settings per database alias "CONNECTION_POOL": { "default": {"minsize": 5, "maxsize": 20}, }, # Logging level "LOG_LEVEL": "WARNING", }
All settings are optional. With no configuration, all models are included and database backends are auto-detected from Django's DATABASES setting.
Supported Field Types
| Django Field | Tortoise Field |
|---|---|
| CharField, SlugField, EmailField, URLField | CharField |
| TextField | TextField |
| IntegerField, BigIntegerField, SmallIntegerField | IntField, BigIntField, SmallIntField |
| BooleanField | BooleanField |
| FloatField | FloatField |
| DecimalField | DecimalField |
| DateField, DateTimeField, TimeField | DateField, DatetimeField, TimeField |
| DurationField | TimeDeltaField |
| UUIDField | UUIDField |
| JSONField | JSONField |
| BinaryField | BinaryField |
| FileField, ImageField | CharField (stores path) |
| ForeignKey | ForeignKeyField |
| OneToOneField | OneToOneField |
| ManyToManyField | ManyToManyField |
| AutoField, BigAutoField, SmallAutoField | IntField/BigIntField/SmallIntField (pk=True) |
ASGI Lifespan
For production ASGI deployments, explicitly manage Tortoise connections:
# asgi.py from django_tortoise import init, close async def lifespan(scope, receive, send): if scope["type"] == "lifespan": while True: message = await receive() if message["type"] == "lifespan.startup": await init() await send({"type": "lifespan.startup.complete"}) elif message["type"] == "lifespan.shutdown": await close() await send({"type": "lifespan.shutdown.complete"}) return
If you don't set up lifespan, connections are initialized lazily on first query.
Generating Static Tortoise Models
By default, Tortoise models are generated dynamically at runtime. If you want static files you can inspect, customize, or version-control, use the management command:
# Generate for all apps python manage.py generate_tortoise_models --output-dir ./tortoise_models # Generate for a specific app python manage.py generate_tortoise_models --app-label demo --output-dir . # Custom Tortoise app name python manage.py generate_tortoise_models --tortoise-app-name myapp
This produces one file per Django app (e.g., tortoise_models_demo.py):
# Auto-generated by django-tortoise-objects. Do not edit manually. import uuid from tortoise import fields from tortoise.fields.relational import OnDelete from tortoise.models import Model class DepartmentTortoise(Model): id = fields.BigIntField(primary_key=True, generated=True) name = fields.CharField(max_length=200) code = fields.CharField(unique=True, max_length=20) budget = fields.DecimalField(default=0, max_digits=14, decimal_places=2) is_active = fields.BooleanField(default=True) class Meta: table = "demo_department" app = "django_tortoise" class TeamTortoise(Model): id = fields.BigIntField(primary_key=True, generated=True) name = fields.CharField(max_length=200) department = fields.ForeignKeyField( "django_tortoise.DepartmentTortoise", related_name='teams', on_delete=OnDelete.CASCADE, source_field='department_id', ) class Meta: table = "demo_team" app = "django_tortoise"
The command respects INCLUDE_MODELS and EXCLUDE_MODELS from your TORTOISE_OBJECTS settings.
Limitations & Non-Goals
Query results are Tortoise model instances, not Django models. Methods like tortoise_objects.get() and tortoise_objects.filter() return Tortoise ORM objects. They cannot be passed directly to Django forms, serializers, admin, or template tags that expect Django model instances. Use tortoise_objects for async read/write paths (APIs, WebSockets, background tasks) and the regular Django ORM for everything else.
This library is not intended to:
- Replace Django ORM — it is a complementary tool for async-critical paths, not a full substitute. Django ORM remains the right choice for admin, forms, management commands, and sync views.
- Manage schema or migrations — all schema management is delegated to Django. Tortoise never writes to your database schema.
- Support cross-ORM transactions — you cannot mix Django and Tortoise queries in a single database transaction.
- Provide Django admin integration — Tortoise query results don't work with Django's admin site. Use Django's ORM for admin.
- Expose Tortoise-specific features — Tortoise signals, custom managers, and validators are not bridged.
- Generate Django models from Tortoise — the bridge is one-way only (Django → Tortoise).
Other things to keep in mind:
- Tortoise maintains its own connection pool, separate from Django's. Configure pool sizes via the
CONNECTION_POOLsetting to avoid excess connections. - Unsupported or custom Django field types are silently skipped during model generation. Check logs at
DEBUGlevel if a field is missing. - ManyToManyField relations require that the related model is also included in the Tortoise bridge (not excluded via
EXCLUDE_MODELS).
Performance
Benchmarks comparing tortoise_objects vs Django's native async ORM (aget, acreate, etc.) on the same models and data. Measured on PostgreSQL (both using psycopg).
PostgreSQL
tortoise_objectswins on single-record ops:get1.3-2.0x,count1.3-1.5x,exists1.5xtortoise_objectswins on writes:create+delete1.4-2.6x- Bulk fetches nearly tied
See example_project/README.md for full benchmark details, methodology, and raw data.
Requirements
- Python >= 3.10
- Django >= 4.2
- Tortoise ORM >= 1.1.2
License
Apache 2.0 — see LICENSE for details.
