SQLAlchemy 2.0: ORM Moderno para Python com Tipagem
Aprenda SQLAlchemy 2.0 com o novo estilo declarativo, Mapped[], suporte async e integração com FastAPI. Guia prático com exemplos reais.
O SQLAlchemy e o ORM mais utilizado no ecossistema Python, presente em projetos que vao de APIs simples a sistemas de grande escala. Com o lancamento da versao 2.0, a biblioteca passou por uma reformulacao significativa: novo estilo declarativo com tipagem nativa, API de queries unificada, suporte a async/await e integracao profunda com ferramentas de analise estatica.
Se voce ja trabalha com PostgreSQL, SQLite ou qualquer banco relacional em Python, entender o SQLAlchemy 2.0 e fundamental. Neste artigo, vamos cobrir desde a configuracao inicial ate padroes avancados como relacionamentos, queries compostas e integracao assincrona.
Por que SQLAlchemy 2.0?
A versao 2.0 nao e uma simples atualizacao incremental. Ela redefine como voce escreve modelos e queries, alinhando o SQLAlchemy com as praticas modernas do Python:
- Tipagem nativa com
Mapped[]emapped_column()— IDEs como VSCode e PyCharm entendem seus modelos sem plugins - API unificada entre Core e ORM — uma unica forma de construir queries
- Async nativo — suporte completo a
async/awaitsem hacks - Melhor performance — uso otimizado de RETURNING e batch inserts
# Estilo antigo (1.x) - ainda funciona, mas nao recomendado
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Usuario(Base):
__tablename__ = "usuarios"
id = Column(Integer, primary_key=True)
nome = Column(String(100))
email = Column(String(200))
# Estilo novo (2.0) - tipado, moderno, recomendado
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Usuario(Base):
__tablename__ = "usuarios"
id: Mapped[int] = mapped_column(primary_key=True)
nome: Mapped[str] = mapped_column(String(100))
email: Mapped[str] = mapped_column(String(200), unique=True)
A diferenca principal e que no estilo 2.0, seus modelos sao classes Python com anotacoes de tipo reais. Ferramentas como mypy e ty conseguem validar seu codigo sem configuracao adicional.
Configuracao inicial
Comece instalando o SQLAlchemy e um driver de banco de dados:
# SQLAlchemy com driver SQLite (incluso no Python)
pip install sqlalchemy
# Para PostgreSQL
pip install sqlalchemy psycopg2-binary
# Para MySQL
pip install sqlalchemy pymysql
# Ou usando uv (mais rapido)
uv pip install sqlalchemy psycopg2-binary
A conexao com o banco usa o objeto Engine:
from sqlalchemy import create_engine
# SQLite (arquivo local)
engine = create_engine("sqlite:///app.db", echo=True)
# PostgreSQL
engine = create_engine(
"postgresql://usuario:senha@localhost:5432/meu_banco",
pool_size=10,
max_overflow=20,
)
# Criar todas as tabelas definidas nos modelos
Base.metadata.create_all(engine)
O parametro echo=True mostra as queries SQL geradas no terminal, util durante o desenvolvimento e depuracao.
Definindo modelos com Mapped[]
O novo estilo declarativo usa Mapped[] para anotar cada coluna. Isso traz beneficios reais para autocompletar e validacao estatica:
from datetime import datetime
from typing import Optional
from sqlalchemy import String, Text, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class Autor(Base):
__tablename__ = "autores"
id: Mapped[int] = mapped_column(primary_key=True)
nome: Mapped[str] = mapped_column(String(100))
email: Mapped[str] = mapped_column(String(200), unique=True)
bio: Mapped[Optional[str]] = mapped_column(Text, default=None)
ativo: Mapped[bool] = mapped_column(default=True)
criado_em: Mapped[datetime] = mapped_column(default_factory=datetime.utcnow)
# Relacionamento: um autor tem muitos artigos
artigos: Mapped[list["Artigo"]] = relationship(back_populates="autor")
def __repr__(self) -> str:
return f"Autor(id={self.id}, nome={self.nome!r})"
class Artigo(Base):
__tablename__ = "artigos"
id: Mapped[int] = mapped_column(primary_key=True)
titulo: Mapped[str] = mapped_column(String(200))
conteudo: Mapped[str] = mapped_column(Text)
publicado: Mapped[bool] = mapped_column(default=False)
autor_id: Mapped[int] = mapped_column(ForeignKey("autores.id"))
criado_em: Mapped[datetime] = mapped_column(default_factory=datetime.utcnow)
# Relacionamento inverso
autor: Mapped["Autor"] = relationship(back_populates="artigos")
def __repr__(self) -> str:
return f"Artigo(id={self.id}, titulo={self.titulo!r})"
Note como Mapped[Optional[str]] indica um campo nullable, enquanto Mapped[str] indica NOT NULL. O type checker consegue inferir isso automaticamente.
CRUD com a nova API de sessions
O SQLAlchemy 2.0 usa Session com context managers para gerenciar transacoes de forma segura:
from sqlalchemy.orm import Session
# CREATE - inserir registros
with Session(engine) as session:
autor = Autor(nome="Ana Silva", email="ana@exemplo.com")
session.add(autor)
artigos = [
Artigo(titulo="Introducao ao Python", conteudo="...", autor=autor),
Artigo(titulo="FastAPI na Pratica", conteudo="...", autor=autor),
]
session.add_all(artigos)
session.commit()
# READ - consultar registros
with Session(engine) as session:
# Buscar por chave primaria
autor = session.get(Autor, 1)
print(autor.nome)
# Query com filtros
from sqlalchemy import select
stmt = select(Autor).where(Autor.ativo == True).order_by(Autor.nome)
autores = session.scalars(stmt).all()
for a in autores:
print(f"{a.nome} - {len(a.artigos)} artigos")
# UPDATE - atualizar registros
with Session(engine) as session:
autor = session.get(Autor, 1)
autor.bio = "Desenvolvedora Python com 5 anos de experiencia"
session.commit()
# DELETE - remover registros
with Session(engine) as session:
artigo = session.get(Artigo, 2)
session.delete(artigo)
session.commit()
O uso de context managers (with) garante que a sessao seja fechada corretamente, mesmo em caso de excecoes.
Queries avancadas com select()
A nova API select() e poderosa e composavel. Voce pode construir queries complexas de forma legivel:
from sqlalchemy import select, func, and_, or_, desc
with Session(engine) as session:
# Contagem de artigos por autor
stmt = (
select(Autor.nome, func.count(Artigo.id).label("total_artigos"))
.join(Artigo)
.group_by(Autor.nome)
.having(func.count(Artigo.id) >= 2)
.order_by(desc("total_artigos"))
)
resultados = session.execute(stmt).all()
for nome, total in resultados:
print(f"{nome}: {total} artigos")
# Busca com filtros combinados
stmt = (
select(Artigo)
.join(Autor)
.where(
and_(
Artigo.publicado == True,
or_(
Artigo.titulo.contains("Python"),
Artigo.titulo.contains("FastAPI"),
),
Autor.ativo == True,
)
)
.order_by(Artigo.criado_em.desc())
.limit(10)
)
artigos = session.scalars(stmt).all()
# Subquery: autores com mais artigos que a media
media_artigos = (
select(func.avg(func.count(Artigo.id)))
.group_by(Artigo.autor_id)
.scalar_subquery()
)
stmt = (
select(Autor)
.join(Artigo)
.group_by(Autor.id)
.having(func.count(Artigo.id) > media_artigos)
)
top_autores = session.scalars(stmt).all()
Essa API e a mesma usada no Core e no ORM, eliminando a confusao que existia na versao 1.x entre session.query() e select().
Suporte async nativo
O SQLAlchemy 2.0 tem suporte nativo a async/await, essencial para frameworks como FastAPI:
from sqlalchemy.ext.asyncio import (
create_async_engine,
async_sessionmaker,
AsyncSession,
)
# Engine assincrono (note o driver asyncpg para PostgreSQL)
async_engine = create_async_engine(
"postgresql+asyncpg://usuario:senha@localhost:5432/meu_banco",
pool_size=10,
)
# Fabrica de sessoes assincronas
AsyncSessionLocal = async_sessionmaker(async_engine, class_=AsyncSession)
async def listar_autores_ativos() -> list[Autor]:
async with AsyncSessionLocal() as session:
stmt = (
select(Autor)
.where(Autor.ativo == True)
.order_by(Autor.nome)
)
resultado = await session.scalars(stmt)
return resultado.all()
async def criar_autor(nome: str, email: str) -> Autor:
async with AsyncSessionLocal() as session:
autor = Autor(nome=nome, email=email)
session.add(autor)
await session.commit()
await session.refresh(autor)
return autor
Para usar com FastAPI, o padrao recomendado e injetar a sessao via dependencia:
from fastapi import FastAPI, Depends
app = FastAPI()
async def get_session():
async with AsyncSessionLocal() as session:
yield session
@app.get("/autores")
async def listar_autores(session: AsyncSession = Depends(get_session)):
stmt = select(Autor).where(Autor.ativo == True)
autores = (await session.scalars(stmt)).all()
return [{"id": a.id, "nome": a.nome} for a in autores]
Migrations com Alembic
Em projetos reais, voce precisa de um sistema de migrations para evoluir o schema do banco sem perder dados. O Alembic e a ferramenta oficial do SQLAlchemy para isso:
# Instalar
pip install alembic
# Inicializar no projeto
alembic init migrations
Configure o alembic.ini com sua connection string e o migrations/env.py para importar seus modelos:
# migrations/env.py (trecho relevante)
from app.models import Base
target_metadata = Base.metadata
Depois, gere e aplique migrations:
# Gerar migration automatica baseada nos modelos
alembic revision --autogenerate -m "criar tabelas iniciais"
# Aplicar migrations pendentes
alembic upgrade head
# Ver historico
alembic history
# Reverter ultima migration
alembic downgrade -1
O Alembic detecta automaticamente mudancas nos seus modelos (novas colunas, tabelas, indices) e gera o script de migration correspondente. Isso e equivalente ao que o Django faz com makemigrations, mas funciona com qualquer framework.
Padroes uteis para projetos reais
Mixin para campos comuns
Evite repeticao usando mixins para campos presentes em todas as tabelas:
from datetime import datetime
from sqlalchemy.orm import Mapped, mapped_column
class TimestampMixin:
criado_em: Mapped[datetime] = mapped_column(default_factory=datetime.utcnow)
atualizado_em: Mapped[datetime] = mapped_column(
default_factory=datetime.utcnow,
onupdate=datetime.utcnow,
)
class Autor(TimestampMixin, Base):
__tablename__ = "autores"
id: Mapped[int] = mapped_column(primary_key=True)
nome: Mapped[str] = mapped_column(String(100))
# criado_em e atualizado_em vem do mixin
Repository pattern
Encapsule a logica de acesso a dados em classes especificas:
class AutorRepository:
def __init__(self, session: Session):
self.session = session
def buscar_por_id(self, autor_id: int) -> Autor | None:
return self.session.get(Autor, autor_id)
def buscar_por_email(self, email: str) -> Autor | None:
stmt = select(Autor).where(Autor.email == email)
return self.session.scalar(stmt)
def listar_ativos(self, limite: int = 50) -> list[Autor]:
stmt = (
select(Autor)
.where(Autor.ativo == True)
.order_by(Autor.nome)
.limit(limite)
)
return list(self.session.scalars(stmt))
def criar(self, nome: str, email: str) -> Autor:
autor = Autor(nome=nome, email=email)
self.session.add(autor)
self.session.flush() # Gera o ID sem commitar
return autor
Esse padrao, combinado com dataclasses ou Pydantic para validacao, cria uma arquitetura limpa e testavel.
Comparativo: SQLAlchemy vs alternativas
| Recurso | SQLAlchemy 2.0 | Django ORM | Tortoise ORM | SQLModel |
|---|---|---|---|---|
| Tipagem nativa | Sim (Mapped[]) | Parcial | Parcial | Sim |
| Async nativo | Sim | Sim (Django 4.1+) | Sim | Sim |
| Migrations | Alembic | Integrado | Aerich | Alembic |
| Independente de framework | Sim | Nao (Django) | Sim | Sim |
| Maturidade | 18+ anos | 18+ anos | 5+ anos | 3+ anos |
| Raw SQL facil | Sim | Limitado | Limitado | Sim |
O SQLAlchemy continua sendo a escolha mais flexivel para projetos que nao usam Django. Se voce trabalha com FastAPI ou Flask, o SQLAlchemy 2.0 e a opcao padrao.
Veja tambem nosso artigo sobre Marimo para explorar dados de forma interativa com notebooks reativos em Python.
Conclusao
O SQLAlchemy 2.0 modernizou uma das bibliotecas mais importantes do ecossistema Python. A tipagem nativa com Mapped[] melhora a experiencia de desenvolvimento de forma tangivel: autocompletar funciona, type checkers encontram erros antes da execucao e o codigo fica mais legivel.
Se voce esta comecando um projeto novo, use o estilo 2.0 desde o inicio. Se tem um projeto existente na versao 1.x, a migracao pode ser gradual — os dois estilos coexistem. O guia oficial de migracao cobre todos os cenarios.
A combinacao de SQLAlchemy 2.0 com Alembic para migrations, Pydantic para validacao e FastAPI para a API cria um stack robusto e moderno para qualquer aplicacao Python que precise de banco de dados relacional.
Equipe python.dev.br
Contribuidor do Python Brasil — Aprenda Python em Português