HomeBlog

Reusable Model Fields in SQLModel with Mixins

7 min read
Diagram showing a mixin class being added to multiple database models, illustrating code reuse.
By David Muraya • September 20 2025

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.

The Problem: Repetitive Fields

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.

The Solution: A Timestamp Mixin

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.

Taking It a Step Further: A Base Model

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.

Conclusion

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:

  • A SoftDeleteMixin that adds an is_deleted flag.
  • An OwnershipMixin that adds a user_id foreign key.

FAQ

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.

Share This Article

About the Author

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.

Related Blog Posts

Enjoyed this blog post? Check out these related posts!

Connecting FastAPI to a Database with SQLModel

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

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

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

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...

On this page

The Problem: Repetitive FieldsThe Solution: A Timestamp MixinTaking It a Step Further: A Base ModelConclusionFAQ

Contact Me

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.

© 2025 David Muraya. All rights reserved.