Skip to content

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:

Featuremapped_columnColumn
Intended LayerORM DeclarativeCore/SQL Expressions
Type Inference✅ From Python type hints❌ Requires explicit type
Nullability Inference✅ From Optional❌ Requires explicit nullable
ORM-Specific FeaturesComplete supportLimited support
Type Checking✅ Mypy compatible❌ Limited support
Boilerplate ReductionMinimal syntaxRequires more parameters
Import Sourcesqlalchemy.ormsqlalchemy

Advantages of mapped_column

Type and Nullability Inference

mapped_column leverages Python type hints to automatically configure column properties:

python
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:

python
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:

python
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:

  1. Core-only tables (without ORM mappers)
  2. Composite column configurations in __table_args__
  3. Legacy codebases using SQLAlchemy 1.x patterns
python
# 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:

python
# 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:

  1. Default to mapped_column for all ORM class attributes
  2. Leverage type hints for automatic type and nullability configuration
  3. 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.