Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 25

SQLAlchemy Models vs. Django Models: A Comparison

Recommended resources

Hello! Welcome back.

In our previous lesson, we successfully established an asynchronous connection between our FastAPI application and a PostgreSQL database. We configured the engine and sessionmaker, and we created a dependency to manage database sessions for each request. We laid the groundwork; now it's time to build upon it.

Today's lesson focuses on this learning outcome: Define database tables using SQLAlchemy declarative models and compare the approach to Django's models.py.

This is where you'll see the direct parallel to creating a models.py file in a Django app. You'll leverage your existing knowledge of defining models, fields, and relationships, but you'll learn SQLAlchemy's modern, type-annotated syntax. We'll pay close attention to the conceptual similarities and the important syntactical differences, just as you requested.

1. The Anatomy of a SQLAlchemy Model

In Django, your models inherit from django.db.models.Model. SQLAlchemy has a similar concept. In the modern, 2.0 style, our models will inherit from a DeclarativeBase. This base class gives our simple Python classes the power of the ORM.

The core components of a SQLAlchemy model are:

  • A Base Class: All our models will inherit from this.
  • __tablename__: An attribute to explicitly name the database table.
  • Type-Annotated Columns: We define columns using Python's type hints combined with SQLAlchemy's Mapped and mapped_column.

This is a significant departure from Django's approach, where table names are often inferred and fields are defined by instantiating models.Field subclasses. SQLAlchemy's modern approach embraces Python's typing system, which provides better static analysis and IDE support.

Let's look at the syntax difference.

Django models.py:




# Django
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=50)
    email = models.EmailField(unique=True)

SQLAlchemy models.py:




# SQLAlchemy
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(50))
    email: Mapped[str] = mapped_column(unique=True)

Notice a few key points in the SQLAlchemy version:

  1. Base class: We define our own Base by inheriting from DeclarativeBase.
  2. __tablename__: We must explicitly name our table.
  3. id primary key: Unlike Django, which automatically adds an id primary key, SQLAlchemy requires you to define it explicitly.
  4. Mapped and mapped_column: The syntax name: Mapped[str] = mapped_column(String(50)) separates the Python type (str), which your IDE and type checkers understand, from the database column definition (mapped_column(...)), which specifies SQL-level details like length and constraints.

To better understand this modern syntax and why it's beneficial, please read the following article. It draws a direct comparison to Django's models, which will be very familiar to you.

SQLAlchemy for Django Developers - Model Definition

This article from OddBird, 'SQLAlchemy for Django Developers', provides a fantastic side-by-side comparison of model definitions.

Read the section titled 'Model Definition: Surprisingly Similar'. Focus on how it contrasts a Django User model with a SQLAlchemy User model, explaining the role of DeclarativeBase, type hints (Mapped), and mapped_column.


2. Defining Columns, Keys, and Relationships

Now that you have the basic structure, let's dive deeper into defining fields and relationships.

Column Types and Constraints

In Django, you use classes like CharField, IntegerField, and BooleanField. SQLAlchemy has corresponding types like String, Integer, and Boolean, which you typically pass into mapped_column.

Common constraints look very similar:

  • primary_key=True
  • unique=True
  • nullable=False (In SQLAlchemy, columns are nullable by default, whereas in Django they are not. You must be explicit.)
  • default=<value>
  • index=True

One key difference from Django is that SQLAlchemy does not automatically create an index for foreign key columns. If you expect to filter or sort by a foreign key often, you should explicitly add index=True.

SQLALchemy vs Django ORM - Keys and Nullability

This article by Alexey Evseev provides a concise breakdown of the differences in how Django and SQLAlchemy handle primary and foreign keys.

Read the sections 'Primary Key', 'Foreign Key', and 'Nullable'. These short sections clearly explain the differences in default behavior that you need to be aware of.

Relationships: The Biggest Difference

This is where the syntax diverges most significantly from Django. In Django, defining a ForeignKey is a one-line operation that handles both the database column and the Python object relationship.

In SQLAlchemy, it's a two-step process:

  1. Define the Foreign Key Column: You create a column that holds the foreign key, using sqlalchemy.ForeignKey.
  2. Define the Relationship Navigation: You use sqlalchemy.orm.relationship to create the Python attribute that allows you to navigate between related objects (e.g., post.author). This function also configures how the relationship loads data and how it connects to the other model.

The back_populates argument in relationship is the direct equivalent of setting a related_name in Django and then having the corresponding accessor on the other side. It creates a bi-directional link.

Let's read a clear explanation of this.

SQLAlchemy for Django Developers - Relations

The OddBird article also has an excellent section on how relationships are defined.

Read the section titled 'Relations Require More Work'. It shows exactly how to set up a one-to-many relationship between a User and Address and contrasts it with the simpler Django ForeignKey syntax.


3. Practical Implementation: Building Our Models

Theory and comparisons are essential, but seeing it all come together is what makes it stick. The following video provides a complete, practical walkthrough of creating a models.py file for a blog application, defining User and Post models with a one-to-many relationship.

This video puts all the pieces we've just discussed into practice.

Python FastAPI Tutorial (Part 5): Adding a Database - SQLAlchemy Models and Relationships

Corey Schafer's tutorial is a great practical guide. This segment shows how to define SQLAlchemy models with columns, types, primary keys, and relationships.

Watch the segment from 08:32 to 16:23. As you watch, pay close attention to: How the User and Post classes inherit from Base. The definition of column types (String, Integer, DateTime). The use of primary_key=True, unique=True, and nullable=False. The two-part definition of the relationship: the user_id ForeignKey column in the Post model, and the relationship() calls in both the User and Post models using back_populates.

After watching, your models.py file for a simple User/Post relationship would look something like this, tying together all the concepts:




# models.py
from datetime import datetime
from sqlalchemy import (
    Column,
    Integer,
    String,
    DateTime,
    ForeignKey,
    func,
)
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from typing import List




# 1. Define the Declarative Base
class Base(DeclarativeBase):
    pass




# 2. Define the User Model
class User(Base):
    __tablename__ = "users"




    # Columns
    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
    email: Mapped[str] = mapped_column(unique=True)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=func.now()
    )




    # Relationship to Post model
    # The `back_populates` string must match the attribute name in the Post model
    posts: Mapped[List["Post"]] = relationship(back_populates="author")




# 3. Define the Post Model
class Post(Base):
    __tablename__ = "posts"




    # Columns
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(100), index=True)
    content: Mapped[str]




    # Foreign Key column linking to the users table
    # This is the actual DB column, analogous to Django's user_id field
    author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    



    # Relationship to User model
    # This provides the `post.author` navigation attribute in Python
    author: Mapped["User"] = relationship(back_populates="posts")
FastAPI, SQLAlchemy, and Alembic Architecture Diagram
This diagram shows where the ORM Models fit into the overall architecture. They form the core of the ORM Layer, translating Python objects into database structures.

4. Creating Tables in the Database

Once your models are defined, you need to instruct SQLAlchemy to create the corresponding tables in the database. As we briefly touched on in the last lesson, you can do this using Base.metadata.create_all().

This is a simple mechanism suitable for development. It checks for the existence of tables before creating them, so it's safe to run multiple times.




# In your main.py or a startup script
from .database import engine
from . import models

async def create_tables():
    async with engine.begin() as conn:



        # This command creates all tables that inherit from models.Base
        await conn.run_sync(models.Base.metadata.create_all)




# You can run this function from a FastAPI lifespan event
# to ensure tables are created when the app starts.

This is functionally similar to running python manage.py migrate for the first time in a new Django app. However, it's not a migration system. It won't handle changes to your models once you have data in your database. That's a job for Alembic, which we'll cover in the next lesson.


Conclusion

Great work! You've now learned how to translate your knowledge of Django models into the world of SQLAlchemy. We've defined the schema for our data using a modern, explicit, and type-safe approach.

Key Takeaways:

  • Explicit is Better than Implicit: SQLAlchemy requires you to be more explicit than Django's ORM (e.g., table names, primary keys, foreign key indexes).
  • Modern, Type-Safe Syntax: Using Mapped and mapped_column provides excellent integration with Python's typing ecosystem, leading to more robust and maintainable code.
  • Relationships are a Two-Step Process: You must define both the ForeignKey column (the database reality) and the relationship attribute (the Python ORM convenience).
  • back_populates is Key: This argument is the SQLAlchemy way to create the bidirectional relationships you're used to in Django.

Preview of the Next Lesson:

Your models are defined, but how do you manage changes to them over time without losing data? In Django, you use makemigrations and migrate. The equivalent in the SQLAlchemy ecosystem is a tool called Alembic. In our next lesson, we will set up and use Alembic for database migrations, comparing the workflow to Django's built-in system.