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.

8 min de leitura Equipe python.dev.br

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[] e mapped_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/await sem 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

RecursoSQLAlchemy 2.0Django ORMTortoise ORMSQLModel
Tipagem nativaSim (Mapped[])ParcialParcialSim
Async nativoSimSim (Django 4.1+)SimSim
MigrationsAlembicIntegradoAerichAlembic
Independente de frameworkSimNao (Django)SimSim
Maturidade18+ anos18+ anos5+ anos3+ anos
Raw SQL facilSimLimitadoLimitadoSim

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.

E

Equipe python.dev.br

Contribuidor do Python Brasil — Aprenda Python em Português