Cheatsheet for derived attributes. Pick the right primitive based on where the computation happens.
Decision matrix
| Tool | Where it runs | When |
|---|---|---|
Python @property | Python only | Read-only, in-memory derivation |
hybrid_property | Python AND SQL | Need to filter/order by the derived value |
column_property | SQL only (fetched at query) | Computed at SELECT, not stored |
Computed (generated col) | DB-side (stored or virtual) | Persisted; indexable |
Plain @property
class User(Base):
first: Mapped[str]
last: Mapped[str]
@property
def full_name(self) -> str:
return f"{self.first} {self.last}"
Use in Python. Can’t use in select(...).where(...).
hybrid_property
from sqlalchemy.ext.hybrid import hybrid_property
class User(Base):
first: Mapped[str]
last: Mapped[str]
@hybrid_property
def full_name(self) -> str:
return f"{self.first} {self.last}"
@full_name.expression
@classmethod
def full_name(cls):
return func.concat(cls.first, " ", cls.last)
# Python
user.full_name # "Alice X"
# SQL
stmt = select(User).where(User.full_name == "Alice X")
Define both: Python implementation + SQL expression.
hybrid_property setter
@hybrid_property
def full_name(self) -> str:
return f"{self.first} {self.last}"
@full_name.setter
def full_name(self, value: str):
self.first, self.last = value.split(" ", 1)
user.full_name = "Alice X" # splits
column_property
from sqlalchemy.orm import column_property
class Order(Base):
quantity: Mapped[int]
unit_price: Mapped[Decimal]
total: Mapped[Decimal] = column_property(
quantity * unit_price,
deferred=False,
)
Adds a computed column to every SELECT: SELECT ..., quantity*unit_price AS total.
Note: column_property defaults to non-deferred (always loaded).
Computed (DB-side)
from sqlalchemy import Computed
class Product(Base):
name: Mapped[str]
name_lower: Mapped[str] = mapped_column(Computed("lower(name)", persisted=True))
DB-side generated column. persisted=True stores (indexable). persisted=False virtual.
Aggregates as column_property
class User(Base):
id: Mapped[int] = mapped_column(primary_key=True)
post_count: Mapped[int] = column_property(
select(func.count(Post.id))
.where(Post.author_id == id)
.correlate_except(Post)
.scalar_subquery(),
deferred=True,
)
Subquery as a column. deferred=True only loads when accessed.
hybrid_method
For methods that take arguments:
from sqlalchemy.ext.hybrid import hybrid_method
class User(Base):
age: Mapped[int]
@hybrid_method
def older_than(self, years: int) -> bool:
return self.age > years
@older_than.expression
@classmethod
def older_than(cls, years: int):
return cls.age > years
# Both work
user.older_than(18)
stmt = select(User).where(User.older_than(18))
Filtering on hybrid_property
# Define expression
@full_name.expression
@classmethod
def full_name(cls):
return func.concat(cls.first, " ", cls.last)
# Filter
stmt = select(User).where(User.full_name.ilike("%alice%"))
# Note: not indexed unless you add an index on (first, last) and write the expression to match
For indexable searches: store + Computed column + GIN trigram index (Postgres).
Ordering
stmt = select(User).order_by(User.full_name)
Same expression used.
Combining with relationships
class Author(Base):
posts: Mapped[list[Post]] = relationship(back_populates="author")
post_count: Mapped[int] = column_property(
select(func.count(Post.id)).where(Post.author_id == id).scalar_subquery(),
deferred=True,
)
deferred=True: doesn’t load with default SELECT. Trigger via undefer(...).
Common mistakes
- Using
@propertythen trying to filter on it →AttributeErrorfrom SQL side. hybrid_propertywithout.expression→ SQL error when used inwhere().column_propertywith non-deferred subquery → every SELECT runs the subquery (slow).- Heavy Computed expressions stored — write amplification.
- Mismatch between Python and SQL expression (e.g., different string concat) — bugs.
FastAPI integration
class UserOut(BaseModel):
id: int
first: str
last: str
full_name: str # populated from hybrid_property
post_count: int # from column_property
model_config = {"from_attributes": True}
For column_property to load: include explicitly:
from sqlalchemy.orm import undefer
stmt = select(User).options(undefer(User.post_count))
Read this next
If you want my hybrid + column_property + Pydantic adapter examples, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .