Tortoise-ORM vs Django vs FastAPI: A Complete Framework Comparison
Choosing the right Python web stack is one of the most consequential architectural decisions a developer can make. Among the most discussed options today are Django, FastAPI, and Tortoise-ORM. While Django is a full-stack framework, FastAPI is a modern async web framework, and Tortoise-ORM is an async ORM that often pairs with FastAPI. Understanding their differences, strengths, and ideal use cases will help you build applications that are maintainable, performant, and scalable.
What Each Tool Actually Is
Before diving into code, it is important to clarify what each of these tools is, because they are not strictly interchangeable.
- Django is a batteries-included web framework. It ships with its own synchronous ORM, admin panel, authentication system, template engine, form handling, and migrations. It is designed to get projects from concept to production quickly.
- FastAPI is a modern, asynchronous web framework built on Starlette and Pydantic. It focuses on speed, type safety, and automatic OpenAPI documentation. It does not include an ORM, leaving that choice to the developer.
- Tortoise-ORM is an async ORM inspired by Django's ORM. It is designed to work natively with async Python, making it a popular companion to FastAPI when developers want Django-like ORM ergonomics without the synchronous overhead.
Why This Comparison Matters
The Python ecosystem has shifted significantly with the rise of async programming. Traditional synchronous frameworks like Django (in its default mode) handle concurrency through threading and process workers, which works well for many workloads but can struggle with high I/O concurrency. FastAPI, being async-native, can handle thousands of concurrent connections efficiently on a single worker. However, an async web framework is only as fast as its slowest component, which is often the database layer. This is where Tortoise-ORM becomes relevant: it provides an async ORM that fits naturally into an async stack.
The comparison matters because choosing Django means committing to its synchronous ORM by default, while choosing FastAPI means you must select an ORM separately. Tortoise-ORM is one of several options (alongside SQLAlchemy 2.0 async, SQLModel, and others), but it is particularly attractive for developers who already know Django's ORM syntax.
Django: The Full-Stack Approach
Django is the oldest and most mature of the three. It follows the "batteries included" philosophy, meaning almost everything you need to build a web application is provided out of the box. This makes Django excellent for content-heavy applications, admin dashboards, and projects where rapid development is a priority.
How to Use Django
Let's look at a practical Django example. We will create a simple blog model, register it with the admin, and expose it through a view.
# models.py
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(
'auth.User',
on_delete=models.CASCADE
)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
# admin.py
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ('title', 'author', 'created_at')
search_fields = ('title', 'content')
# views.py
from django.http import JsonResponse
from .models import Post
def post_list(request):
posts = Post.objects.values('id', 'title', 'created_at')
return JsonResponse(list(posts), safe=False)
# urls.py
from django.urls import path
from .views import post_list
urlpatterns = [
path('api/posts/', post_list, name='post_list'),
]
After defining the model, you run python manage.py makemigrations and python manage.py migrate to create the database schema. Django handles the entire migration lifecycle for you.
Django Strengths and Trade-offs
- Strengths: Admin panel, mature ORM, large ecosystem, excellent documentation, built-in auth, form handling, and security features.
- Trade-offs: Synchronous by default (though Django 3.1+ supports async views), heavier framework, less control over individual components, and the ORM can become a bottleneck under extreme I/O concurrency.
FastAPI: The Modern Async Framework
FastAPI is built for performance and developer experience. It leverages Python type hints for request validation, response serialization, and automatic documentation generation. It is async-native, meaning it can handle many concurrent I/O operations efficiently.
How to Use FastAPI
Here is a basic FastAPI application. Note that FastAPI does not include an ORM, so this example uses in-memory storage for simplicity. We will integrate Tortoise-ORM in the next section.
# main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List
from datetime import datetime
app = FastAPI(title="Blog API")
class PostCreate(BaseModel):
title: str
content: str
author: str
class PostResponse(BaseModel):
id: int
title: str
content: str
author: str
created_at: datetime
# In-memory storage for demonstration
posts_db: List[dict] = []
counter = 0
@app.get("/posts", response_model=List[PostResponse])
async def get_posts():
return posts_db
@app.post("/posts", response_model=PostResponse, status_code=201)
async def create_post(post: PostCreate):
global counter
counter += 1
new_post = {
"id": counter,
"title": post.title,
"content": post.content,
"author": post.author,
"created_at": datetime.utcnow(),
}
posts_db.append(new_post)
return new_post
@app.get("/posts/{post_id}", response_model=PostResponse)
async def get_post(post_id: int):
for post in posts_db:
if post["id"] == post_id:
return post
raise HTTPException(status_code=404, detail="Post not found")
Run this with uvicorn main:app --reload. FastAPI automatically generates interactive API documentation at /docs (Swagger UI) and /redoc.
FastAPI Strengths and Trade-offs
- Strengths: Exceptional performance, automatic validation and serialization, auto-generated docs, native async support, type-safe development.
- Trade-offs: No built-in ORM, admin panel, or auth system. You must assemble these yourself. This gives flexibility but requires more setup work.
Tortoise-ORM: Async ORM with Django Familiarity
Tortoise-ORM bridges the gap between Django's familiar ORM syntax and the async world. It supports PostgreSQL, MySQL, SQLite, and MariaDB. Its query syntax will feel immediately familiar to Django developers.
How to Use Tortoise-ORM with FastAPI
Let's build the same blog API, but this time using Tortoise-ORM for database persistence. First, install the required packages:
pip install fastapi uvicorn tortoise-orm aerich
Now, define the models and wire up Tortoise with FastAPI:
# main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
from tortoise import fields
from tortoise.models import Model
from tortoise.contrib.fastapi import register_tortoise
app = FastAPI(title="Blog API with Tortoise-ORM")
# Tortoise model
class Post(Model):
id = fields.IntField(pk=True)
title = fields.CharField(max_length=200)
content = fields.TextField()
author = fields.CharField(max_length=100)
created_at = fields.DatetimeField(auto_now_add=True)
class Meta:
table = "posts"
# Pydantic schemas
class PostIn(BaseModel):
title: str
content: str
author: str
class PostOut(BaseModel):
id: int
title: str
content: str
author: str
created_at: datetime
class Config:
from_attributes = True
# Register Tortoise with FastAPI
register_tortoise(
app,
db_url="sqlite://db.sqlite3",
modules={"models": ["main"]},
generate_schemas=True,
add_exception_handlers=True,
)
@app.get("/posts", response_model=List[PostOut])
async def get_posts():
return await Post.all()
@app.post("/posts", response_model=PostOut, status_code=201)
async def create_post(post: PostIn):
new_post = await Post.create(
title=post.title,
content=post.content,
author=post.author,
)
return new_post
@app.get("/posts/{post_id}", response_model=PostOut)
async def get_post(post_id: int):
post = await Post.get_or_none(id=post_id)
if post is None:
raise HTTPException(status_code=404, detail="Post not found")
return post
@app.put("/posts/{post_id}", response_model=PostOut)
async def update_post(post_id: int, post: PostIn):
existing = await Post.get_or_none(id=post_id)
if existing is None:
raise HTTPException(status_code=404, detail="Post not found")
existing.title = post.title
existing.content = post.content
existing.author = post.author
await existing.save()
return existing
@app.delete("/posts/{post_id}", status_code=204)
async def delete_post(post_id: int):
post = await Post.get_or_none(id=post_id)
if post is None:
raise HTTPException(status_code=404, detail="Post not found")
await post.delete()
The register_tortoise helper handles startup and shutdown events, initializing the database connection when the app starts and closing it when the app shuts down. The generate_schemas=True flag creates tables automatically, which is convenient for development. In production, you should use a migration tool like aerich.
Using Aerich for Migrations
For production-grade migration management, use Aerich, which is Tortoise-ORM's migration tool:
# aerich.ini
[aerich]
tortoise_orm = main.TORTOISE_ORM
location = ./migrations
# settings.py
TORTOISE_ORM = {
"connections": {"default": "sqlite://db.sqlite3"},
"apps": {
"models": {
"models": ["main", "aerich.models"],
"default_connection": "default",
}
},
}
# Initialize and create first migration
aerich init -t aerich.settings.TORTOISE_ORM
aerich init-db
aerich migrate
aerich upgrade
Direct Comparison: When to Choose What
Performance Characteristics
FastAPI with Tortoise-ORM generally outperforms Django in I/O-bound scenarios because both the web layer and database layer are async. A single Uvicorn worker can handle many concurrent database queries without blocking. Django, being synchronous by default, requires multiple worker processes or threads to handle concurrent requests, which consumes more memory.
However, for CPU-bound workloads, the difference is less significant. Django can also use async views (available since Django 3.1), but its ORM remains synchronous, which limits the benefit.
Development Speed
Django wins on initial development speed for CRUD-heavy applications. The admin panel alone can save days of work. FastAPI requires more setup for equivalent functionality, but the automatic documentation and type validation reduce bugs and improve API quality.
Ecosystem and Community
- Django: Massive ecosystem, thousands of packages, decades of community knowledge, extensive third-party integrations.
- FastAPI: Rapidly growing community, excellent documentation, strong adoption in modern microservices and API-first projects.
- Tortoise-ORM: Smaller community than Django's ORM or SQLAlchemy, but actively maintained and growing alongside FastAPI's popularity.
Best Practices
For Django Projects
- Use
select_relatedandprefetch_relatedto avoid N+1 query problems. - Keep business logic in models or services, not in views.
- Use Django REST Framework for API development rather than writing raw JSON responses.
- Enable connection pooling (e.g., PgBouncer) for production database connections.
- Use async views sparingly and only where the ORM is not the bottleneck.
For FastAPI Projects
- Always use
async deffor route handlers that perform I/O. Usedefonly for CPU-bound handlers that should run in a threadpool. - Separate Pydantic schemas from ORM models to maintain clean boundaries.
- Use dependency injection for database sessions, authentication, and configuration.
- Structure your project with routers and modules rather than putting everything in one file.
- Validate environment variables with Pydantic Settings at startup.
For Tortoise-ORM Projects
- Use
fetch_relatedorprefetch_relatedto avoid N+1 queries, just as you would in Django. - Always use a migration tool like Aerich in production. Never rely on
generate_schemasin production. - Configure connection pooling appropriately for your database backend.
- Use transactions (
async with in_transaction():) for multi-step write operations. - Define
Metaclasses explicitly with table names to avoid naming surprises.
Example: Using Transactions in Tortoise-ORM
from tortoise.transactions import in_transaction
async def transfer_credits(from_id: int, to_id: int, amount: float):
async with in_transaction():
sender = await Account.get(id=from_id)
receiver = await Account.get(id=to_id)
if sender.balance < amount:
raise ValueError("Insufficient balance")
sender.balance -= amount
receiver.balance += amount
await sender.save()
await receiver.save()
Putting It All Together
The choice between Django, FastAPI, and Tortoise-ORM depends on your project requirements. If you need a complete, opinionated framework with an admin panel and want to ship quickly, Django is the right choice. If you are building a high-performance API and want fine-grained control over every component, FastAPI is ideal. If you choose FastAPI but want a Django-like ORM experience with async support, Tortoise-ORM is a natural fit. Many teams even combine FastAPI with Tortoise-ORM to get the best of both worlds: modern async performance with familiar ORM ergonomics. Ultimately, the best framework is the one that aligns with your team's expertise, your performance requirements, and the long-term maintainability goals of your project.