Overview
SQLAlchemy’s declarative system provides the foundation for defining ORM mappings. It consists of three main components: DeclarativeBase, the legacy declarative_base() function, and the registry object.
DeclarativeBase
The modern, type-checker friendly way to create a declarative base class.
Basic Usage
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
# Now use Base as the parent for all mapped classes
class User(Base):
__tablename__ = "users"
# ... column definitions
DeclarativeBase was introduced in SQLAlchemy 2.0 and is the recommended approach for new projects as it provides better IDE support and type checking.
Configuration Options
Customize the base class with class-level attributes:
Class Attributes
Optional MetaData collection for Table objects. If not specified, a new MetaData is created automatically.
Optional pre-configured registry. If not specified, a new registry is created using the metadata and type_annotation_map.
Maps Python types to SQLAlchemy type engines. Used by mapped_column() to derive SQL types from annotations.
declarative_base() Function
The legacy function-based approach to creating a declarative base.
While declarative_base() still works in SQLAlchemy 2.0+, it is superseded by DeclarativeBase. Use DeclarativeBase for new projects to get better type checking support.
Basic Usage
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "users"
# ... column definitions
Function Signature
def declarative_base(
*,
metadata: Optional[MetaData] = None,
mapper: Optional[Callable[..., Mapper]] = None,
cls: Type[Any] = object,
name: str = "Base",
class_registry: Optional[dict] = None,
type_annotation_map: Optional[dict] = None,
constructor: Callable[..., None] = _declarative_constructor,
metaclass: Type[Any] = DeclarativeMeta,
) -> Any:
Optional MetaData instance. If None, a blank MetaData is created.
Base class to use. Defaults to object. Can be a tuple of classes.
Display name for the generated base class. Useful for debugging.
Dictionary mapping Python types to SQLAlchemy types.
Custom __init__ implementation. Defaults to a constructor that assigns keyword arguments to attributes.
metaclass
type
default:"DeclarativeMeta"
Metaclass to use for the base class.
Examples
Custom Constructor
def my_constructor(self, **kwargs):
"""Custom initialization logic"""
for key, value in kwargs.items():
setattr(self, key, value)
self.initialized = True
Base = declarative_base(constructor=my_constructor)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
# Custom constructor is used
user = User(name="Alice")
assert user.initialized == True
Shared Registry
# Share class registry between multiple bases
shared_registry = {}
Base1 = declarative_base(class_registry=shared_registry)
Base2 = declarative_base(class_registry=shared_registry)
class User(Base1):
__tablename__ = "users"
# ...
class Address(Base2):
__tablename__ = "addresses"
# Can reference User by string name
user_id = Column(ForeignKey("users.id"))
user = relationship("User") # Works across bases
registry Object
The registry is the core object that manages all ORM mappings.
Creating a Registry
from sqlalchemy.orm import registry
# Create a registry
reg = registry()
# Use with DeclarativeBase
class Base(DeclarativeBase):
registry = reg
# Or use directly with mapper functions
@reg.mapped
class User:
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
Constructor Parameters
class registry:
def __init__(
self,
*,
metadata: Optional[MetaData] = None,
class_registry: Optional[dict] = None,
type_annotation_map: Optional[dict] = None,
constructor: Callable[..., None] = _declarative_constructor,
):
MetaData collection for declarative table mapping. If None, creates a blank MetaData.
Optional shared registry for string-based class name lookups in relationships.
Maps Python types to SQLAlchemy TypeEngine classes for use with Mapped[] annotations.
Default __init__ for mapped classes without their own __init__.
Registry Methods
generate_base()
Create a declarative base class from the registry:
reg = registry()
Base = reg.generate_base()
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
mapped()
Decorator for declarative mapping without a base class:
reg = registry()
@reg.mapped
class User:
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
map_imperatively()
Imperatively map a class to a table:
from sqlalchemy import Table, Column, Integer, String
reg = registry()
metadata = MetaData()
user_table = Table(
"users",
metadata,
Column("id", Integer, primary_key=True),
Column("name", String),
)
class User:
pass
reg.map_imperatively(User, user_table)
Configure all unconfigured mappers:
reg = registry()
# Define classes...
# Configure all relationships
reg.configure()
If True, also configure dependent registries that are linked via relationships.
dispose()
Dispose of all mappers in the registry:
reg.dispose(cascade=True)
Registry Properties
Read-only collection of all Mapper objects in this registry.for mapper in reg.mappers:
print(mapper.class_)
The MetaData collection associated with this registry.# Create all tables
reg.metadata.create_all(engine)
Type mapping dictionary that can be updated:from sqlalchemy import String
reg.update_type_annotation_map({
str: String(50)
})
Advanced Configuration
Multiple Registries
Use multiple registries for different database schemas or purposes:
from sqlalchemy.orm import registry
# Registry for application data
app_registry = registry(metadata=MetaData(schema="app"))
# Registry for audit/logging data
audit_registry = registry(metadata=MetaData(schema="audit"))
class AppBase(DeclarativeBase):
registry = app_registry
class AuditBase(DeclarativeBase):
registry = audit_registry
class User(AppBase):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
class UserAudit(AuditBase):
__tablename__ = "user_audit"
id: Mapped[int] = mapped_column(primary_key=True)
Custom Type Maps
Define application-wide type conventions:
from decimal import Decimal
from datetime import datetime
from sqlalchemy import String, Numeric, DateTime
from typing import Annotated
# Define annotated types
str50 = Annotated[str, "varchar50"]
str255 = Annotated[str, "varchar255"]
money = Annotated[Decimal, "money"]
class Base(DeclarativeBase):
type_annotation_map = {
str50: String(50),
str255: String(255),
money: Numeric(10, 2),
datetime: DateTime(timezone=True),
}
class Product(Base):
__tablename__ = "products"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str255] # VARCHAR(255)
sku: Mapped[str50] # VARCHAR(50)
price: Mapped[money] # NUMERIC(10, 2)
created_at: Mapped[datetime] # TIMESTAMP WITH TIME ZONE
Dynamic Tablename
Use declared_attr for dynamic configuration:
from sqlalchemy.orm import declared_attr
class TableNameMixin:
@declared_attr.directive
def __tablename__(cls) -> str:
# Auto-generate tablename from class name
return cls.__name__.lower()
class Base(DeclarativeBase):
pass
class UserAccount(TableNameMixin, Base):
# __tablename__ will be "useraccount"
id: Mapped[int] = mapped_column(primary_key=True)
class ProductItem(TableNameMixin, Base):
# __tablename__ will be "productitem"
id: Mapped[int] = mapped_column(primary_key=True)
Comparison: Old vs New
Modern (2.0+)
Legacy (1.x)
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
email: Mapped[Optional[str]]
Benefits:
- Full type checking support
- Better IDE autocomplete
- Clearer intent with Mapped[] annotations
- No need to import Column, Integer, String, etc.
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
email = Column(String, nullable=True)
Limitations:
- No type hints for mypy/pyright
- Harder to understand nullability
- More verbose with explicit Column objects
Best Practices
- Use DeclarativeBase for all new projects
- Centralize type mappings in the base class
- Use mixins for common patterns across models
- Keep registry per logical database when using multiple databases
See Also