Pydantic Settings: Configuração Segura em Python
Aprenda a usar pydantic-settings para configurar aplicações Python com variáveis de ambiente, .env, validação, testes e boas práticas para produção.
Configuração parece um detalhe pequeno até o primeiro deploy falhar porque uma variável de ambiente estava com nome errado, uma URL de banco veio vazia ou um token real foi parar no repositório. Em projetos Python, esse problema aparece em APIs com FastAPI, jobs de dados, automações internas, bots, CLIs, pipelines e aplicações que precisam rodar de forma diferente no notebook, no Docker, no CI e em produção.
O erro comum é espalhar os.getenv() pelo código inteiro. No começo funciona. Depois cada módulo passa a ler uma variável diferente, os valores não são validados, o padrão local vira comportamento implícito e ninguém sabe quais configurações são obrigatórias. Quando a aplicação cresce, configuração deixa de ser conveniência e vira contrato.
O pydantic-settings resolve esse ponto usando a validação do Pydantic para carregar variáveis de ambiente, arquivos .env e valores tipados em uma classe única. Em vez de descobrir erro em produção, você falha cedo com uma mensagem clara. Para quem trabalha com FastAPI, Docker em projetos Python, testes com pytest ou SQLModel com FastAPI, esse é um padrão simples que melhora segurança, manutenção e confiança no deploy.
Quando usar pydantic-settings
Use pydantic-settings quando sua aplicação precisa ler configuração externa. Alguns exemplos:
| Cenário | Configurações comuns |
|---|---|
| API FastAPI | URL do banco, ambiente, CORS, chave JWT |
| Job de dados | caminho de entrada, bucket, API externa, timeout |
| Bot ou automação | token, canal, modo debug, limite de retries |
| CLI interna | endpoint, formato de saída, diretório de cache |
| Testes e CI | banco temporário, flags de feature, credenciais falsas |
Para um script descartável de 20 linhas, talvez os.getenv() seja suficiente. Para qualquer projeto que vai para GitHub, currículo, cliente, time ou produção, vale centralizar. Isso comunica maturidade: você não está apenas fazendo o código rodar na sua máquina, está preparando o projeto para ambientes diferentes.
Instalação e primeiro Settings
Instale o pacote:
uv add pydantic-settings
Ou com pip:
pip install pydantic-settings
Crie um arquivo app/settings.py:
from functools import lru_cache
from pydantic import AnyUrl, Field, PostgresDsn
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
app_name: str = "API Python Brasil"
environment: str = Field(default="local", alias="APP_ENV")
debug: bool = False
database_url: PostgresDsn
public_url: AnyUrl = "http://localhost:8000"
request_timeout_seconds: int = Field(default=10, ge=1, le=60)
@lru_cache
def get_settings() -> Settings:
return Settings()
Esse exemplo já mostra a diferença para os.getenv(). database_url é obrigatório e precisa ter formato de DSN PostgreSQL. request_timeout_seconds vira inteiro e só aceita valores entre 1 e 60. debug entende valores comuns de booleano vindos do ambiente. Se algo vier inválido, a aplicação falha no início, antes de abrir servidor, worker ou pipeline.
Um .env.example seguro poderia ser:
APP_ENV=local
DEBUG=false
DATABASE_URL=postgresql://usuario:senha@localhost:5432/app
PUBLIC_URL=http://localhost:8000
REQUEST_TIMEOUT_SECONDS=10
Nunca faça commit do .env real. Faça commit apenas de .env.example, com nomes de variáveis e valores falsos. Essa prática aparece em qualquer checklist profissional de segurança porque evita vazamento de token, senha de banco, segredo JWT e credencial de API.
Usando com FastAPI
Em uma API, injete a configuração de forma explícita:
from fastapi import Depends, FastAPI
from app.settings import Settings, get_settings
app = FastAPI()
@app.get("/health")
def health(settings: Settings = Depends(get_settings)) -> dict[str, str]:
return {
"app": settings.app_name,
"environment": settings.environment,
}
Esse padrão evita importar uma instância global mutável em todos os arquivos. O lru_cache garante que a classe seja criada uma vez por processo, mas ainda permite sobrescrever a dependência nos testes. Para projetos maiores, você pode passar settings.database_url para a criação do engine SQLAlchemy ou SQLModel, configurar CORS a partir de uma lista validada e ajustar integrações externas por ambiente.
Separando configuração de segredo
Nem toda configuração é segredo. APP_ENV=production ou REQUEST_TIMEOUT_SECONDS=10 podem aparecer em logs e documentação. Já DATABASE_URL, JWT_SECRET_KEY, OPENAI_API_KEY, SMTP_PASSWORD e tokens de webhook devem ser tratados com cuidado.
Uma boa regra é:
- configuração operacional pode ir para
.env.example, README e painel de deploy; - segredo real deve ficar no gerenciador de secrets do provedor, no CI ou em ferramenta específica;
- logs nunca devem imprimir valores sensíveis;
- testes devem usar credenciais falsas ou bancos temporários.
Se você usa Docker Compose local, pode carregar .env para desenvolvimento, mas em produção prefira o mecanismo do provedor: variáveis do Cloud Run, secrets do Kubernetes, GitHub Actions Secrets, Gitea Actions Secrets, AWS Secrets Manager, Infisical, Doppler ou equivalente. O pydantic-settings não substitui um cofre de segredos; ele valida e organiza o que chega ao processo Python.
Configuração por ambiente
Evite criar muitos arquivos mágicos como .env.prod, .env.staging e .env.local sem documentação. Em geral, o mais simples é manter uma classe única e trocar valores por ambiente:
class Settings(BaseSettings):
environment: str = Field(default="local", alias="APP_ENV")
debug: bool = False
database_url: PostgresDsn
sentry_dsn: AnyUrl | None = None
@property
def is_production(self) -> bool:
return self.environment == "production"
No código, tome decisões explícitas:
settings = get_settings()
if settings.is_production and settings.debug:
raise RuntimeError("DEBUG não deve ficar ativo em produção")
Esse tipo de trava parece simples, mas evita incidentes reais. Configuração é parte da confiabilidade da aplicação. Em APIs e jobs, também vale validar timeouts, limites de lote, URLs externas e flags de feature para não aceitar valores perigosos por engano.
Testando configurações
Testes precisam provar que a aplicação falha quando configuração obrigatória está ausente e carrega corretamente quando as variáveis existem. Com pytest, use monkeypatch:
import pytest
from pydantic import ValidationError
from app.settings import Settings
def test_settings_carrega_database_url(monkeypatch):
monkeypatch.setenv("DATABASE_URL", "postgresql://user:pass@localhost:5432/app")
settings = Settings()
assert str(settings.database_url).startswith("postgresql://user:pass@localhost")
def test_settings_exige_database_url(monkeypatch):
monkeypatch.delenv("DATABASE_URL", raising=False)
with pytest.raises(ValidationError):
Settings()
Se você usa get_settings() com lru_cache, limpe o cache entre testes que mudam ambiente:
from app.settings import get_settings
def test_cache(monkeypatch):
get_settings.cache_clear()
monkeypatch.setenv("DATABASE_URL", "postgresql://user:pass@localhost:5432/app")
assert get_settings().environment == "local"
get_settings.cache_clear()
Essa disciplina evita testes intermitentes, especialmente quando a suíte roda no CI com variáveis diferentes da máquina local.
Boas práticas para projetos reais
Mantenha a classe Settings pequena o suficiente para ser entendida. Se ela virar um arquivo enorme, separe por contexto: DatabaseSettings, AuthSettings, EmailSettings e uma configuração principal que compõe tudo. Ainda assim, evite abstração cedo demais; uma classe simples resolve a maior parte dos projetos.
Use nomes claros e estáveis. DATABASE_URL é melhor do que DB, APP_ENV é melhor do que MODE, PUBLIC_URL é melhor do que URL. Documente cada variável obrigatória no README e mantenha .env.example sincronizado. Quando remover uma variável, remova também da documentação.
Também vale validar listas e URLs com tipos específicos. Em vez de aceitar qualquer string para CORS, modele uma lista de URLs permitidas. Em vez de aceitar timeout sem limite, use Field(ge=1, le=60). Em vez de aceitar ambiente arbitrário, use Literal["local", "staging", "production"] quando fizer sentido.
Como isso melhora seu portfólio
Para quem busca vaga Python no Brasil, pydantic-settings é um detalhe que diferencia projeto de curso de projeto profissional. Um recrutador técnico ou pessoa entrevistadora percebe quando o README explica variáveis, o .env.example existe, os segredos não estão no GitHub, os testes cobrem configuração e a aplicação falha de forma previsível.
Esse padrão combina bem com APIs, automações, pipelines e projetos de dados. Uma API com FastAPI e SQLModel fica mais convincente quando tem configuração tipada. Um pipeline de ETL com Python fica mais seguro quando URL de API, timeout e destino de carga não estão hardcoded. Um projeto em Docker fica mais realista quando separa .env.example, Compose local e variáveis de produção.
Se o projeto crescer para microsserviços ou ferramentas de infraestrutura, também faz sentido estudar como outras linguagens lidam com configuração. Em serviços Go, por exemplo, é comum combinar variáveis de ambiente, structs de configuração e validação no startup; veja o ecossistema em Go Brasil para comparar padrões de backend.
Checklist final
Antes de publicar um projeto Python, revise:
- existe uma classe central de configuração;
- variáveis obrigatórias são validadas no startup;
.env.exampleestá no repositório;.envreal está no.gitignore;- segredos reais não aparecem em README, testes ou logs;
- testes cobrem pelo menos o caminho feliz e a variável obrigatória ausente;
- Docker, CI e deploy usam os mesmos nomes de variáveis.
Pydantic-settings não é uma ferramenta chamativa, mas resolve uma dor recorrente. Ele transforma configuração em contrato tipado, reduz surpresa entre ambientes e deixa seu projeto mais fácil de operar. Para aplicações Python em 2026, essa é uma das pequenas práticas que mais passam sensação de código pronto para uso real.
Equipe Python Brasil
Contribuidor do Python Brasil — Aprenda Python em Português