Contexto
Pydantic e marshmallow resolvem o mesmo problema de fundo: transformar dados soltos (um JSON de request, um dicionário vindo do banco, variáveis de ambiente) em estruturas validadas e tipadas, e depois de volta em algo serializável. O que separa os dois é como você declara o schema. Pydantic parte dos type hints nativos do Python: você escreve uma classe que herda de BaseModel, anota cada campo com o tipo esperado, e o próprio type hint vira a regra de validação. No v2, essa validação roda sobre um núcleo compilado em Rust (pydantic-core), o que coloca a biblioteca numa faixa de performance que Python puro não alcança. O v2 foi uma reescrita grande justamente para mover o miolo de validação para esse núcleo.
marshmallow segue o caminho oposto e explícito: você declara uma classe Schema e lista cada campo como um objeto (fields.Str(), fields.Int(), fields.Nested(...)), sem depender de type hints, e a (de)serialização acontece em métodos separados, load() e dump(). É uma biblioteca mais antiga e madura, pensada desde o começo para ser independente de qualquer framework.
A variável decisiva é essa: você quer que o schema seja o próprio type hint do Python (Pydantic) ou uma declaração explícita e independente do sistema de tipos (marshmallow)? Quase todo o resto (velocidade, integração com IDE, encaixe em frameworks) decorre dessa escolha.
Quando escolher Pydantic
Pydantic faz mais sentido quando os dados já vivem como objetos tipados no seu código e você quer que validação e tipagem sejam a mesma coisa. É a escolha default de qualquer projeto FastAPI: o framework usa Pydantic como base para validar o request e serializar o response, então o modelo que você escreve para a API é o mesmo que o type checker enxerga. mypy e o autocomplete da IDE entendem o modelo sem plugin nenhum, porque não há mágica de runtime: os campos são atributos anotados de uma classe.
from pydantic import BaseModel, EmailStr, field_validator
class User(BaseModel):
id: int
name: str
email: EmailStr
is_active: bool = True
@field_validator("name")
@classmethod
def name_not_blank(cls, v: str) -> str:
if not v.strip():
raise ValueError("name must not be blank")
return v
user = User(id="42", name="Ada", email="ada@example.com")
# id arrived as "42" (str) and was coerced to 42 (int)
user.model_dump()
# {'id': 42, 'name': 'Ada', 'email': 'ada@example.com', 'is_active': True}
O exemplo mostra o comportamento típico do v2: id chega como a string "42" e é convertido para int, a validação roda na construção do objeto, e model_dump() cuida da serialização de volta para um dicionário. Erros vêm num ValidationError estruturado, com o caminho do campo e o tipo do erro, fácil de devolver como resposta de API. Para configuração, o pacote irmão pydantic-settings carrega variáveis de ambiente e arquivos .env direto em um BaseSettings tipado, o que substitui o clássico os.environ espalhado pelo código. Na versão 2.13.4, com mais de 1,29 bilhão de downloads por semana no PyPI, Pydantic é hoje a camada de validação default do Python moderno, puxada em boa parte pela adoção de FastAPI.
Quando escolher marshmallow
marshmallow brilha quando você quer uma camada de (de)serialização explícita, independente de framework e do sistema de tipos. Ele não assume nada sobre como seus dados vivem no código: você descreve o formato externo (o payload) num Schema, e as direções de entrada e saída ficam separadas de propósito. load() pega dados crus e valida/desserializa; dump() pega seus objetos e serializa. Essa separação é útil quando as duas direções não são simétricas, por exemplo um campo write-only como senha, que entra no load mas nunca sai no dump.
from marshmallow import Schema, fields, validate, ValidationError
class UserSchema(Schema):
id = fields.Int(required=True)
name = fields.Str(required=True, validate=validate.Length(min=1))
email = fields.Email(required=True)
is_active = fields.Bool(load_default=True)
schema = UserSchema()
try:
user = schema.load({"id": 42, "name": "Ada", "email": "ada@example.com"})
except ValidationError as err:
print(err.messages) # {'field': ['error message']}
# dump() goes the other way, from the object back to a serializable dict
payload = schema.dump(user)
O Schema acima não sabe nada sobre type hints: cada campo é um objeto declarado em runtime, o que o torna menos visível ao mypy e ao autocomplete do que um BaseModel, mas também totalmente desacoplado de qualquer framework. É por isso que marshmallow é comum no mundo Flask (via flask-marshmallow) e em qualquer stack que não gire em torno de FastAPI. Erros de validação chegam em ValidationError.messages, um dicionário de mensagens por campo. Na versão 4.3.0, com cerca de 154 milhões de downloads por semana, marshmallow carrega anos de estrada e um ecossistema testado em produção (marshmallow-sqlalchemy, integração com Flask, APISpec), o tipo de maturidade que não se improvisa.
Veredito
Reduzida ao essencial, a escolha tem uma variável central: como o schema se relaciona com o sistema de tipos do Python. Se seus dados já vivem como objetos tipados, ou se o projeto roda em FastAPI, Pydantic é o encaixe óbvio. O v2 trouxe o núcleo em Rust que resolveu a crítica histórica de performance, os modelos são type hints de verdade (então mypy e IDE entendem tudo sem plugin), e o pydantic-settings fecha o caso de uso de configuração tipada. É essa combinação, mais do que qualquer número isolado, que fez Pydantic virar o default do ecossistema.
marshmallow continua sendo a escolha certa quando você quer uma camada de (de)serialização explícita e agnóstica de framework, quando as direções de load e dump são assimétricas, ou quando o projeto já roda uma stack Flask madura com flask-marshmallow e integrações testadas. A diferença de tração é real (1,29 bilhão contra 154 milhões de downloads por semana), mas ela reflete sobretudo o efeito FastAPI puxando Pydantic, não uma fragilidade de marshmallow. Migrar um Schema que já funciona só para ganhar type hints raramente compensa; começar um serviço FastAPI novo em marshmallow é remar contra a corrente do framework. Escolha pela forma de declarar o schema, não pela contagem de downloads.