Hybrid properties and computed columns

SQLAlchemy · Advanced · 12 min

Keeping logic like full_name or is_overdue in one place, usable in Python and in WHERE clauses, avoids duplicated rules.

A `hybrid_property` is a property whose getter runs on instances like any Python property, but when accessed on the class it builds a SQL expression. So user.full_name returns a string and User.full_name == "Ada L" works inside where().

If the Python body also works on columns (string concatenation, arithmetic, comparisons), one function covers both. When it doesn't, for example it uses an if, Python-only functions or len(), add a separate SQL version with @full_name.inplace.expression (or the older @full_name.expression), returning an expression built from class attributes, often with case() or func.

Hybrids can also have setters (@x.inplace.setter) to split a value into columns.

Related tools:

  • column_property(select(...).scalar_subquery()) adds a read-only attribute loaded with the object via SQL, like a comment count. It's computed in every query that loads the entity, so use deferred=True for expensive ones.
  • Computed("price * qty") declares a database-generated column, stored or virtual depending on the database.
  • Plain @property is fine when you never query on the value.

Hybrids are evaluated in Python for loaded objects and in SQL in queries, so make sure both paths agree, especially around NULL handling.

Example

from sqlalchemy import create_engine, select, case
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase): pass
class Account(Base):
    __tablename__ = "accounts"
    id: Mapped[int] = mapped_column(primary_key=True)
    balance: Mapped[int]

    @hybrid_property
    def status(self) -> str:
        return "overdrawn" if self.balance < 0 else "ok"

    @status.inplace.expression
    @classmethod
    def _status_expr(cls):
        return case((cls.balance < 0, "overdrawn"), else_="ok")

engine = create_engine("sqlite://"); Base.metadata.create_all(engine)
with Session(engine) as s:
    s.add_all([Account(balance=50), Account(balance=-5)]); s.commit()
    print(s.get(Account, 2).status)                                   # overdrawn
    print(s.scalars(select(Account.id).where(Account.status == "ok")).all())  # [1]

Remember

  • hybrid_property works on instances (Python) and on the class (SQL)
  • Add an .inplace.expression when the Python logic can't render as SQL
  • column_property for SQL-computed attributes loaded with the object
  • Keep the Python and SQL versions consistent, including NULLs

Watch out for

  • Using Python if in a hybrid without an expression version, so queries silently use the wrong branch or error.

Check yourself

  1. Why does the example define a separate expression for status?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.sqlalchemy.org/en/20/orm/extensions/hybrid.html