← Back to DevBytes

Tortoise-ORM vs Django vs FastAPI: Framework Comparison

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.

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

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

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

Best Practices

For Django Projects

For FastAPI Projects

For Tortoise-ORM Projects

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles