---
title: "SQLAlchemy 2.0: ORM Moderno para Python com Tipagem"
url: "https://python.dev.br/blog/sqlalchemy-2-orm-moderno-python/"
markdown_url: "https://python.dev.br/blog/sqlalchemy-2-orm-moderno-python.MD"
description: "Aprenda SQLAlchemy 2.0 com o novo estilo declarativo tipado com Mapped[], suporte async nativo e integração com FastAPI. Guia prático com exemplos."
date: "2026-04-24"
author: "Equipe python.dev.br"
---

# SQLAlchemy 2.0: ORM Moderno para Python com Tipagem

Aprenda SQLAlchemy 2.0 com o novo estilo declarativo tipado com Mapped[], suporte async nativo e integração com FastAPI. Guia prático com exemplos.


O **SQLAlchemy** é o ORM mais utilizado no ecossistema Python, presente em projetos que vão de APIs simples a sistemas de grande escala. Com o lançamento da versão 2.0, a biblioteca passou por uma reformulação significativa: novo estilo declarativo com tipagem nativa, API de queries unificada, suporte a async/await e integração profunda com ferramentas de análise estática.

Se você já trabalha com [PostgreSQL](/blog/python-e-postgresql/), [SQLite](/blog/python-e-banco-de-dados-sqlite/) ou qualquer banco relacional em Python, entender o SQLAlchemy 2.0 é fundamental. Neste artigo, vamos cobrir desde a configuração inicial até padrões avançados como relacionamentos, queries compostas e integração assíncrona.

## Por que SQLAlchemy 2.0?

A versão 2.0 não é uma simples atualização incremental. Ela redefine como você escreve modelos e queries, alinhando o SQLAlchemy com as práticas 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 única forma de construir queries
- **Async nativo** — suporte completo a `async/await` sem hacks
- **Melhor performance** — uso otimizado de RETURNING e batch inserts

```python
# Estilo antigo (1.x) - ainda funciona, mas não 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 diferença principal é que no estilo 2.0, seus modelos são [classes Python](/glossario/classe/) com anotações de tipo reais. Ferramentas como [mypy](/blog/tipagem-estatica-python-mypy/) e [ty](/blog/ty-type-checker-python-rust/) conseguem validar seu código sem configuração adicional.

## Configuração inicial

Comece instalando o SQLAlchemy e um driver de banco de dados:

```bash
# 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 rápido)
uv pip install sqlalchemy psycopg2-binary
```

A conexão com o banco usa o objeto `Engine`:

```python
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 parâmetro `echo=True` mostra as queries SQL geradas no terminal, útil durante o [desenvolvimento e depuração](/glossario/logging/).

## Definindo modelos com Mapped[]

O novo estilo declarativo usa `Mapped[]` para anotar cada coluna. Isso traz benefícios reais para autocompletar e validação estática:

```python
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](/blog/tipagem-estatica-python-mypy/) consegue inferir isso automaticamente.

## CRUD com a nova API de sessions

O SQLAlchemy 2.0 usa `Session` com context managers para gerenciar transações de forma segura:

```python
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="Introdução ao Python", conteudo="...", autor=autor),
        Artigo(titulo="FastAPI na Prática", conteudo="...", autor=autor),
    ]
    session.add_all(artigos)
    session.commit()

# READ - consultar registros
with Session(engine) as session:
    # Buscar por chave primária
    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 experiência"
    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](/glossario/context-manager/) (`with`) garante que a sessão seja fechada corretamente, mesmo em caso de exceções.

## Queries avançadas com select()

A nova API `select()` é poderosa e composável. Você pode construir queries complexas de forma legível:

```python
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 média
    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 é a mesma usada no Core e no ORM, eliminando a confusão que existia na versão 1.x entre `session.query()` e `select()`.

## Suporte async nativo

O SQLAlchemy 2.0 tem suporte nativo a [async/await](/glossario/async-await/), essencial para frameworks como [FastAPI](/blog/apis-rest-com-fastapi/):

```python
from sqlalchemy.ext.asyncio import (
    create_async_engine,
    async_sessionmaker,
    AsyncSession,
)

# Engine assíncrono (note o driver asyncpg para PostgreSQL)
async_engine = create_async_engine(
    "postgresql+asyncpg://usuario:senha@localhost:5432/meu_banco",
    pool_size=10,
)

# Fábrica de sessões assíncronas
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 padrão recomendado é injetar a sessão via dependência:

```python
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, você precisa de um sistema de migrations para evoluir o schema do banco sem perder dados. O **Alembic** é a ferramenta oficial do SQLAlchemy para isso:

```bash
# 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:

```python
# migrations/env.py (trecho relevante)
from app.models import Base

target_metadata = Base.metadata
```

Depois, gere e aplique migrations:

```bash
# Gerar migration automática baseada nos modelos
alembic revision --autogenerate -m "criar tabelas iniciais"

# Aplicar migrations pendentes
alembic upgrade head

# Ver histórico
alembic history

# Reverter última migration
alembic downgrade -1
```

O Alembic detecta automaticamente mudanças nos seus modelos (novas colunas, tabelas, índices) e gera o script de migration correspondente. Isso é equivalente ao que o Django faz com `makemigrations`, mas funciona com qualquer framework.

## Padrões úteis para projetos reais

### Mixin para campos comuns

Evite repetição usando mixins para campos presentes em todas as tabelas:

```python
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 vêm do mixin
```

### Repository pattern

Encapsule a lógica de acesso a dados em classes específicas:

```python
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 padrão, combinado com [dataclasses](/blog/dataclasses-python-guia-completo/) ou [Pydantic](/blog/pydantic-validacao-dados-python/) para validação, cria uma arquitetura limpa e testável.

## 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 | Não (Django) | Sim | Sim |
| Maturidade | 18+ anos | 18+ anos | 5+ anos | 3+ anos |
| Raw SQL fácil | Sim | Limitado | Limitado | Sim |

O SQLAlchemy continua sendo a escolha mais flexível para projetos que não usam Django. Se você trabalha com [FastAPI](/glossario/fastapi/) ou [Flask](/glossario/flask/), o SQLAlchemy 2.0 é a opção padrão.

Veja também nosso artigo sobre [Marimo](/blog/marimo-notebook-reativo-python/) para explorar dados de forma interativa com notebooks reativos em Python.

## Conclusão

O SQLAlchemy 2.0 modernizou uma das bibliotecas mais importantes do ecossistema Python. A tipagem nativa com `Mapped[]` melhora a experiência de desenvolvimento de forma tangível: autocompletar funciona, type checkers encontram erros antes da execução e o código fica mais legível.

Se você está começando um projeto novo, use o estilo 2.0 desde o início. Se tem um projeto existente na versão 1.x, a migração pode ser gradual — os dois estilos coexistem. O [guia oficial de migração](https://docs.sqlalchemy.org/en/21/changelog/migration_20.html) cobre todos os cenários.

A combinação de SQLAlchemy 2.0 com Alembic para migrations, [Pydantic](/blog/pydantic-validacao-dados-python/) para validação e [FastAPI](/blog/apis-rest-com-fastapi/) para a API cria um stack robusto e moderno para qualquer aplicação Python que precise de banco de dados relacional.

Se você trabalha com múltiplas linguagens, vale conhecer também o <a href="https://golang.com.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">GORM em Go</a> e o <a href="https://kotlin.dev.br/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'kotlin.dev.br' })">Exposed em Kotlin</a> — ORMs modernos com type safety que seguem filosofias semelhantes ao SQLAlchemy 2.0.
