Defining Database Models with SQLAlchemy Declarative
A SQLAlchemy model is a Python class that represents a database table. Each model class inherits from the declarative base, maps to a single table, and uses class attributes to define columns. Defining models correctly ensures data integrity, query efficiency, and schema clarity.
Basic Model Structure
Create a new file models.py and define a simple model:
from sqlalchemy import Column, Integer, String, DateTime, create_engine
from sqlalchemy.orm import declarative_base
from datetime import datetime
Base = declarative_base()
class User(Base):
__tablename__ = "users"
# Primary key column
id = Column(Integer, primary_key=True, autoincrement=True)
# String columns with length constraints
username = Column(String(50), unique=True, nullable=False)
email = Column(String(120), unique=True, nullable=False)
# Optional column with default value
created_at = Column(DateTime, default=datetime.utcnow)
def __repr__(self):
return f"<User(id={self.id}, username={self.username})>"
Every model requires __tablename__ (maps the class to a database table) and at least one column marked primary_key=True. The primary_key=True argument ensures the column uniquely identifies each row. The autoincrement=True flag (default for integer primary keys) automatically generates sequential IDs.
Column Types and Constraints
SQLAlchemy provides data types for every common database type. Common types and their uses:
| Type | Example | Use Case |
|---|---|---|
Integer | Column(Integer) | Whole numbers (user IDs, counts) |
String(n) | Column(String(100)) | Text with max length n |
Text | Column(Text) | Unlimited text (descriptions, articles) |
Float | Column(Float) | Decimal numbers (prices, ratings) |
Boolean | Column(Boolean) | True/False flags |
DateTime | Column(DateTime) | Timestamps (created_at, updated_at) |
Date | Column(Date) | Calendar dates (birth_date) |
JSON | Column(JSON) | Nested objects (metadata, preferences) |
Constraints enforce data rules at the database level, preventing invalid data from being inserted:
from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime
from datetime import datetime
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True)
# String with length; nullable=False enforces NOT NULL
name = Column(String(200), nullable=False)
# Float with check constraint (price >= 0)
price = Column(Float, nullable=False)
# Boolean with default value
is_active = Column(Boolean, default=True)
# DateTime with server-side default
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# Unique constraint prevents duplicate values
sku = Column(String(50), unique=True, nullable=False)
The nullable=False argument makes a column required (NOT NULL in SQL). The unique=True argument ensures no two rows have the same value in that column. The default argument provides a Python-callable or static value used when inserting a new row without specifying that column.
Primary Keys and Indexing
A primary key uniquely identifies each row and is the fastest column to search. SQLAlchemy creates an index on the primary key automatically. For tables where you want to search by a non-primary column frequently, add an explicit index:
from sqlalchemy import Column, Integer, String, Index
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True)
title = Column(String(300), nullable=False)
slug = Column(String(300), unique=True, nullable=False)
# Explicit index for fast filtering
author_id = Column(Integer, nullable=False)
__table_args__ = (
Index("idx_author_id", "author_id"),
Index("idx_slug", "slug"),
)
The __table_args__ tuple allows you to specify indexes, constraints, and other table-level settings. Indexes speed up WHERE clauses and JOIN conditions; create them on foreign keys and frequently filtered columns.
Default Values and Server-Side Defaults
A default value is used when a row is inserted without specifying that column. Use Python callables for computed defaults:
from sqlalchemy import Column, Integer, String, DateTime, func
from datetime import datetime
class Comment(Base):
__tablename__ = "comments"
id = Column(Integer, primary_key=True)
text = Column(String(1000), nullable=False)
# Python callable (called at insert time)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# SQL function (executed by the database)
updated_at = Column(DateTime, onupdate=func.now(), nullable=False)
The default=datetime.utcnow (without parentheses) tells SQLAlchemy to call datetime.utcnow() when a new Comment is created. The onupdate=func.now() uses a SQL function to set updated_at whenever the row is modified.
Table Relationships (Foreign Keys Overview)
Models often reference other models using foreign keys. The foreign key column stores the primary key of another table, creating a relationship. We'll explore relationships in depth in the next article; here's a preview:
from sqlalchemy import Column, Integer, String, ForeignKey
class Author(Base):
__tablename__ = "authors"
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
class Book(Base):
__tablename__ = "books"
id = Column(Integer, primary_key=True)
title = Column(String(300), nullable=False)
# Foreign key: author_id references authors.id
author_id = Column(Integer, ForeignKey("authors.id"), nullable=False)
The ForeignKey("authors.id") argument tells SQLAlchemy (and the database) that author_id references the id column in the authors table. This enforces referential integrity: you cannot insert a book with an author_id that doesn't exist.
Creating Tables in the Database
Once your models are defined, create the actual tables using the declarative base:
from sqlalchemy import create_engine
from models import Base
engine = create_engine("sqlite:///myapp.db")
Base.metadata.create_all(engine) # Creates all tables
Calling Base.metadata.create_all(engine) inspects all models that inherit from Base and creates corresponding tables in the database. If a table already exists, it is skipped; this is safe to call on every application startup.
Key Takeaways
- Models are classes that inherit from
declarative_base()and map to database tables via__tablename__. - Every model requires a primary key (usually
id = Column(Integer, primary_key=True)). - Constraints (nullable, unique, default, check) enforce data integrity at the database level.
- Indexes on frequently searched or joined columns speed up queries.
- Foreign keys reference other tables and enforce referential integrity.
- Call
Base.metadata.create_all(engine)to create all tables in the database.
Frequently Asked Questions
Should I use String(50) or Text for long text fields?
Use String(n) for fields with a known maximum length (usernames, emails, URLs) because the database enforces the limit. Use Text for variable-length content (article bodies, descriptions) with no enforced maximum. Text is not slower in modern databases.
What's the difference between default and onupdate?
default sets a value when inserting a new row if no value is provided. onupdate sets a value whenever the row is updated (modified). Use onupdate=func.now() for automatic "last modified" timestamps.
Do I need to specify a primary key, or can I skip it?
SQLAlchemy requires every model to have a primary key. If your table represents a junction table for many-to-many relationships, use a composite primary key of two foreign keys.
Can I use the same model name in different modules?
Yes. Model names (like User) are local to your module. However, keep them unique site-wide for clarity. The actual table name is determined by __tablename__, so two User classes with different table names will create separate tables.
How do I handle datetime columns with timezones?
Use DateTime(timezone=True) to store timezone-aware timestamps. Store datetimes in UTC and convert to local time in your application.