| name | tortoise-orm |
| description | Build Python applications using Tortoise ORM v1.x, the async Python ORM inspired by Django. Use when writing or modifying Tortoise Model definitions, queries, relations (ForeignKeyField, ManyToManyField, OneToOneField), transactions, signals, validators, Pydantic integration, or Tortoise initialization. Also use when the user mentions Tortoise ORM, tortoise-orm, async ORM with asyncpg/asyncmy, fields.ForeignKeyField, fields.ManyToManyField, prefetch_related, select_related, Tortoise.init, RegisterTortoise, tortoise.contrib.pydantic, or any Tortoise ORM patterns, even if they don't explicitly say 'Tortoise.' Covers the full lifecycle, model definition, CRUD, relations, querying, transactions, signals, migrations, testing, and Pydantic serialization. |
Tortoise ORM v1.x
Async Python ORM inspired by Django, supporting PostgreSQL (asyncpg/psycopg), MySQL (asyncmy/aiomysql), SQLite, and MSSQL.
Initialization
from tortoise import Tortoise
await Tortoise.init(
db_url="sqlite://db.sqlite3",
modules={"models": ["app.models"]},
)
await Tortoise.generate_schemas()
Multi-database config uses a dict with connections and apps keys. See references/api-reference.md for the full config structure.
FastAPI integration (v1.x lifespan pattern):
from contextlib import asynccontextmanager
from tortoise.contrib.fastapi import RegisterTortoise
@asynccontextmanager
async def lifespan(app: FastAPI):
async with RegisterTortoise(
app, db_url="sqlite://db.sqlite3",
modules={"models": ["app.models"]},
generate_schemas=True,
):
yield
app = FastAPI(lifespan=lifespan)
Connection access (v1.0+ pattern):
conn = Tortoise.get_connection("default")
await Tortoise.close_connections()
The old from tortoise import connections import is deprecated in v1.0+. Always use Tortoise.get_connection().
Defining Models
from tortoise.models import Model
from tortoise import fields
class Tournament(Model):
id = fields.IntField(primary_key=True)
name = fields.CharField(max_length=255)
created = fields.DatetimeField(auto_now_add=True)
class Meta:
table = "tournaments"
ordering = ["name"]
If no primary key field is defined, an auto-incrementing IntField named id is created. Access any PK via the .pk alias.
Meta options: table, schema, abstract, unique_together, indexes, ordering, table_description, fetch_db_defaults, manager.
Inheritance works via abstract base classes or mixins:
class TimestampMixin:
created_at = fields.DatetimeField(null=True, auto_now_add=True)
modified_at = fields.DatetimeField(null=True, auto_now=True)
class AbstractBase(Model):
class Meta:
abstract = True
class User(TimestampMixin, AbstractBase):
name = fields.CharField(max_length=100)
Fields
Common parameters: null, default (supports callables), unique, db_index, source_field (custom DB column name), description, validators, generated, primary_key.
| Category | Fields |
|---|
| Numeric | IntField, BigIntField, SmallIntField, FloatField, DecimalField(max_digits, decimal_places) |
| Text | CharField(max_length), TextField, BinaryField |
| Temporal | DateField, DatetimeField(auto_now, auto_now_add), TimeField, TimeDeltaField |
| Other | BooleanField, UUIDField, JSONField(encoder, decoder), CharEnumField(enum_type), IntEnumField(enum_type) |
auto_now / auto_now_add are Python-only -- they do NOT create a DB DEFAULT clause. For database-level defaults, use db_default:
from tortoise.fields.db_defaults import Now, SqlDefault
fields.DatetimeField(db_default=Now())
fields.IntField(db_default=SqlDefault("0"))
Relations
ForeignKey:
class Event(Model):
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events", on_delete=fields.OnDelete.CASCADE
)
- DB column is
tournament_id -- accessible directly for performance (avoids fetching the related object)
on_delete options: CASCADE, RESTRICT, SET_NULL (requires null=True), SET_DEFAULT, NO_ACTION
ManyToMany:
participants: fields.ManyToManyRelation["Team"] = fields.ManyToManyField(
"models.Team", related_name="events", through="event_team"
)
Both objects must be saved before calling .add().
OneToOne:
address: fields.OneToOneRelation[Address] = fields.OneToOneField(
"models.Address", on_delete=fields.OnDelete.CASCADE, related_name="event"
)
Reverse relations -- annotate them for type hints:
class Tournament(Model):
events: fields.ReverseRelation["Event"]
Querying
QuerySets are lazy -- they build queries but don't execute until awaited.
obj = await Model.create(name="foo")
obj = Model(name="foo"); await obj.save()
obj = await Model.get(id=1)
obj = await Model.get_or_none(id=1)
items = await Model.filter(name="foo")
items = await Model.all()
await Model.filter(id=1).update(name="bar")
obj.name = "bar"; await obj.save()
await Model.filter(id=1).delete()
await obj.delete()
obj, created = await Model.get_or_create(name="x", defaults={"field": "val"})
obj, created = await Model.update_or_create(name="x", defaults={"field": "val"})
QuerySet methods: filter, exclude, all, first, last, count, exists, values, values_list, only, defer, order_by, limit, offset, distinct, group_by, annotate, select_related, prefetch_related, select_for_update, using_db, raw, explain.
Filter operators (double-underscore): __gt, __gte, __lt, __lte, __in, __not_in, __not, __isnull, __contains, __icontains, __startswith, __istartswith, __endswith, __iendswith, __iexact, __search, __range, __year, __month, __day, etc.
Q objects for complex boolean logic:
from tortoise.expressions import Q
await Event.filter(Q(id__in=[1, 2]) | Q(name="3"))
await Event.filter(Q(name="x") & ~Q(tournament__name="y"))
F expressions for field references:
from tortoise.expressions import F
await Model.filter(field_a__gt=F("field_b"))
await Model.filter(id=1).update(counter=F("counter") + 1)
Aggregation:
from tortoise.functions import Count, Sum, Avg, Max, Min
result = await Book.annotate(count=Count("id")).group_by("author_id").values("author_id", "count")
Bulk operations:
await Model.bulk_create([obj1, obj2], batch_size=100)
await Model.bulk_update([obj1, obj2], fields=["name"], batch_size=100)
Fetching Relations
Three approaches -- choose based on context:
| Method | Scope | When to use |
|---|
select_related("fk_field") | QuerySet (JOIN) | FK/O2O, small result sets |
prefetch_related("m2m_field") | QuerySet (separate queries) | M2M, reverse FK, nested |
await obj.fetch_related("field") | Single instance | After you already have the object |
Custom prefetch filtering:
from tortoise.query_utils import Prefetch
await Tournament.all().prefetch_related(
Prefetch("events", queryset=Event.filter(name="First"))
)
Cross-relation filtering:
await Team.filter(events__tournament__id=tournament.id)
Transactions
from tortoise.transactions import in_transaction, atomic
async with in_transaction() as connection:
await MyModel.create(name="foo")
@atomic()
async def create_pair(data):
user = await User.create(**data)
await Profile.create(user=user)
Nested in_transaction blocks create savepoints. Multi-DB: pass connection_name parameter.
Signals
from tortoise.signals import pre_save, post_save, pre_delete, post_delete
@post_save(MyModel)
async def on_save(sender, instance, created, using_db, update_fields):
if created:
print(f"New: {instance.id}")
The created flag on post_save indicates INSERT vs UPDATE. Signal handlers must be imported before they can fire.
Pydantic Integration
from tortoise.contrib.pydantic import pydantic_model_creator, pydantic_queryset_creator
Tournament_Pydantic = pydantic_model_creator(Tournament)
Tournament_PydanticIn = pydantic_model_creator(Tournament, exclude_readonly=True)
pydantic_obj = await Tournament_Pydantic.from_tortoise_orm(instance)
pydantic_obj = await Tournament_Pydantic.from_queryset_single(Tournament.get(id=1))
Call Tortoise.init_models(["app.models"], "models") before creating Pydantic models if full ORM init hasn't happened yet. Computed fields in PydanticMeta must have explicit return type hints.
Testing (v1.x -- pytest only)
The old unittest-based TestCase/IsolatedTestCase classes are removed in v1.0+.
import pytest_asyncio
from tortoise.contrib.test import tortoise_test_context
@pytest_asyncio.fixture
async def db():
async with tortoise_test_context(["myapp.models"]) as ctx:
yield ctx
@pytest.mark.asyncio
async def test_create(db):
user = await User.create(name="Test")
assert user.name == "Test"
Migrations (v1.1.7 built-in)
Aerich is legacy. Tortoise v1.1.7 has a built-in migration system:
tortoise init
tortoise makemigrations
tortoise migrate
tortoise downgrade models
tortoise history
Data migrations use RunPython (with apps.get_model()) or RunSQL.
Critical Rules
These are the most common mistakes. Violating them causes bugs that are hard to trace.
1. Always await QuerySets
QuerySets are lazy. Model.filter(name="x") returns a QuerySet object, not results. You must await it.
users = Model.filter(active=True)
users = await Model.filter(active=True)
2. Always fetch relations before accessing them
Accessing an unfetched relation raises NoValuesFetched. There are no lazy-loading shortcuts.
event = await Event.get(id=1)
print(event.tournament.name)
event = await Event.get(id=1)
await event.fetch_related("tournament")
print(event.tournament.name)
event = await Event.all().select_related("tournament").get(id=1)
3. Never use generate_schemas() in production
It is for development only. Use the migration system for production deployments.
4. Never run concurrent transactions inside asyncio.gather()
Transactions are stateful and expect sequential execution. Nesting transaction blocks inside gather() or concurrent tasks causes data corruption.
5. Use Tortoise.get_connection(), not from tortoise import connections
The connections import is deprecated in v1.0+.
6. Save both objects before M2M .add()
You cannot add to a ManyToMany relation if either object has not been persisted yet.
7. Understand auto_now vs db_default
auto_now/auto_now_add are Python-side only. They do not create DB DEFAULT clauses. If you need the database to set defaults (for raw SQL inserts, migrations, etc.), use db_default=Now().
8. Call init_models() before Pydantic model creation
If you create Pydantic models before full ORM init, relations will not resolve. Call Tortoise.init_models() first.
For the complete field reference, DB URL formats, engine names, exception hierarchy, validator reference, and advanced patterns, see references/api-reference.md.