mapped_column vs Column in SQLAlchemy
Problem Statement
If you're working with SQLAlchemy 2.0+, you've likely encountered both mapped_column()
and Column()
for defining table attributes. These similar-looking constructs cause confusion—when should you use each? What's the difference, and why would you choose one over the other?
The confusion arises from SQLAlchemy's layered architecture: Column
originates from the Core/SQL expression layer, while mapped_column
is designed for the ORM Declarative layer. Using the proper construct ensures you get full functionality and cleaner code.
Key Differences
Here's a quick reference table showing the core differences:
Feature | mapped_column | Column |
---|---|---|
Intended Layer | ORM Declarative | Core/SQL Expressions |
Type Inference | ✅ From Python type hints | ❌ Requires explicit type |
Nullability Inference | ✅ From Optional | ❌ Requires explicit nullable |
ORM-Specific Features | Complete support | Limited support |
Type Checking | ✅ Mypy compatible | ❌ Limited support |
Boilerplate Reduction | Minimal syntax | Requires more parameters |
Import Source | sqlalchemy.orm | sqlalchemy |
Advantages of mapped_column
Type and Nullability Inference
mapped_column
leverages Python type hints to automatically configure column properties:
from sqlalchemy.orm import Mapped, mapped_column
from typing import Optional
class User(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] # Inferred: VARCHAR NOT NULL
email: Mapped[str] = mapped_column(unique=True)
is_admin: Mapped[bool] # Inferred: BOOLEAN NOT NULL
last_login: Mapped[Optional[datetime]] # Inferred: DATETIME NULLABLE
TIP
Nullability is determined automatically: Mapped[Optional[...]]
translates to nullable=True
, while Mapped[...]
defaults to nullable=False
.
Reduced Boilerplate
When no configuration is needed beyond the type hint, you can omit mapped_column
entirely:
class Settings(Base):
__tablename__ = 'settings'
# Equivalent to: Mapped[int] = mapped_column(primary_key=True)
id: Mapped[int] = mapped_column(primary_key=True)
# Minimal declaration (inferred as INTEGER NOT NULL)
user_id: Mapped[int]
# Configured explicitly
dark_mode: Mapped[bool] = mapped_column(default=False)
Type Checking Support
Using Mapped with type hints enables robust IDE and mypy type checking:
user = session.get(User, 1)
print(user.name) # ✅ Correct: name is str
print(user.is_admin) # ✅ Correct: is_admin is bool
print(user.missing) # ❌ Error: "User" has no attribute "missing"
ORM-Specific Features
mapped_column
supports ORM-only functionality like deferred loading and active history tracking that don't belong in the Core layer.
When to Use Column
While mapped_column
is preferred for ORM models, Column
is still required for:
- Core-only tables (without ORM mappers)
- Composite column configurations in
__table_args__
- Legacy codebases using SQLAlchemy 1.x patterns
# Core Table Example (non-ORM)
from sqlalchemy import Table, Column, Integer, String
user_table = Table('users', metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50))
)
Compatibility Note
Column
still works in ORM declarative models alongside mapped_column
, but you lose type inference and nullability automation.
Migrating from Column to mapped_column
To upgrade 1.x-style code to SQLAlchemy 2.0:
# Old approach (1.x)
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50), nullable=False)
# New approach (2.0+)
class User(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50)) # Explicit length config
# For existing databases with schema changes enabled
metadata.reflect(engine)
Base = automap_base(metadata=metadata)
Solution Recommendation
For new SQLAlchemy 2.0+ projects using ORM declarative models:
- Default to
mapped_column
for all ORM class attributes - Leverage type hints for automatic type and nullability configuration
- Reserve
Column
for Core constructs and legacy integrations
Adopting mapped_column
yields more concise, type-safe, and maintainable ORM code while properly separating Core and ORM concerns in SQLAlchemy's architecture.