In the previous guide, we built a complete CRUD application using FastAPI and SQLModel. As you build more complex applications, you'll notice you're repeating the same fields across different models. For example, almost every table needs an id
, a created_at
timestamp, and an updated_at
timestamp.
This is where mixins come in. A mixin is a class that provides fields and methods to other classes, but isn't meant to be used on its own. Think of it as a reusable "feature pack" you can add to your models to avoid writing the same code over and over.
Let's look at the Product
model from our last article.
class Product(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(index=True) price: float description: Optional[str] = None
Now, imagine you also need an Order
model. It would probably look something like this:
class Order(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) quantity: int product_id: int = Field(foreign_key="product.id")
Both models have an id
field. If we wanted to track when they were created and updated, we'd have to add created_at
and updated_at
fields to both, leading to duplicated code.
We can create a TimestampMixin
to provide the created_at
and updated_at
fields. This mixin won't be a table itself; it just holds the fields we want to reuse.
# In a file like app/services/database/mixins.py from datetime import datetime, timezone from sqlalchemy.dialects.postgresql import TIMESTAMP from sqlmodel import Field, SQLModel def utcnow(): """Returns the current time in UTC.""" return datetime.now(timezone.utc) class TimestampMixin(SQLModel): """A mixin to add created_at and updated_at timestamp fields to a model.""" created_at: datetime = Field( default_factory=utcnow, nullable=False, sa_type=TIMESTAMP(timezone=True) ) updated_at: datetime = Field( default_factory=utcnow, nullable=False, sa_column_kwargs={"onupdate": utcnow}, sa_type=TIMESTAMP(timezone=True) )
Let's break this down:
utcnow
: A simple function that returns the current time with the UTC timezone. Using timezone-aware datetimes is crucial for consistency.default_factory=utcnow
: This tells SQLModel to call the utcnow
function to get a default value when a new record is created.sa_column_kwargs={"onupdate": utcnow}
: This is a powerful SQLAlchemy feature. It tells the database to automatically call the utcnow
function at the database level whenever the record is updated. This is more efficient than doing it in your application code.sa_type=TIMESTAMP(timezone=True)
: This explicitly tells SQLAlchemy to use PostgreSQL's timezone-aware TIMESTAMP
type, ensuring data is stored correctly.Now, we can add this mixin to our Product
model.
from .mixins import TimestampMixin class Product(TimestampMixin, SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(index=True) price: float description: Optional[str] = None
Just by adding TimestampMixin
to the inheritance list, our Product
model now has created_at
and updated_at
fields.
After modifying your models, you must create a database migration to apply these schema changes. For a detailed guide on this process, see my article on Running Database Migrations with Alembic in Google Cloud Build.
We can make this even cleaner. The id
field is also repeated in every model. Let's create a BaseModel
that includes the id
and our timestamp mixin.
# In a file like app/services/database/models.py from typing import Optional from .mixins import TimestampMixin class BaseModel(SQLModel): """A base model with an ID field.""" id: Optional[int] = Field(default=None, primary_key=True, index=True) class TimestampedBaseModel(BaseModel, TimestampMixin): """A base model with ID, created_at, and updated_at fields.""" pass
Now, TimestampedBaseModel
is a reusable base that provides everything we need for a standard table. Our Product
model becomes much simpler.
# In app/services/database/models.py from .base import TimestampedBaseModel class Product(TimestampedBaseModel, table=True): name: str = Field(index=True) price: float description: Optional[str] = None
This is much cleaner. Any new model we create can now inherit from TimestampedBaseModel
to automatically get an id
, created_at
, and updated_at
field, all with the correct database-level configurations.
Mixins are a simple but powerful tool for writing DRY (Don't Repeat Yourself) code in your data layer. By creating reusable "feature packs" for common fields like IDs and timestamps, you can make your models cleaner, more consistent, and easier to maintain.
You can extend this pattern for other common features, such as:
SoftDeleteMixin
that adds an is_deleted
flag.OwnershipMixin
that adds a user_id
foreign key.Q: Why not just put all the fields in one big base class? A: You could, but mixins give you more flexibility. They allow you to compose features. For example, some models might need timestamps but not a soft delete feature. With mixins, you can pick and choose which functionality to add to each model.
Q: What does sa_column_kwargs
do?
A: It's a way to pass arguments directly to the underlying SQLAlchemy Column
object. This lets you access powerful database-specific features, like the onupdate
trigger, that aren't directly exposed in SQLModel's Field
.
Q: Is onupdate
better than updating the timestamp in my Python code?
A: Yes. Using the database-level onupdate
trigger is more reliable and efficient. It guarantees the updated_at
field is always current, even if updates happen outside of your application code (e.g., a direct database query).
Q: Can a mixin have methods?
A: Absolutely. Mixins can contain both fields and methods. For example, a SoftDeleteMixin
could have a soft_delete()
method that sets the is_deleted
flag to True
.
David Muraya is a Solutions Architect specializing in Python, FastAPI, and Cloud Infrastructure. He is passionate about building scalable, production-ready applications and sharing his knowledge with the developer community. You can connect with him on LinkedIn.
Enjoyed this blog post? Check out these related posts!
Connecting FastAPI to a Database with SQLModel
A practical guide to building CRUD APIs with FastAPI and SQLModel.
Read More...
Centralizing Your FastAPI Configuration Settings with Pydantic Settings
How to Organize and Load FastAPI Configuration Settings from a .env File Using Pydantic Settings
Read More...
Connecting FastAPI to a Serverless Database with pool_pre_ping
How to Keep FastAPI Connected to Serverless Databases Like Neon
Read More...
Building Resilient Task Queues in FastAPI with ARQ Retries
How to Handle Failures and Implement Robust Retry Logic in FastAPI Background Jobs with ARQ.
Read More...
Have a project in mind? Send me an email at hello@davidmuraya.com and let's bring your ideas to life. I am always available for exciting discussions.