Configurações espalhadas em chamadas a os.getenv() produzem valores sem validação e erros tardios. pydantic-settings reúne as opções em um modelo tipado, converte valores de ambiente e interrompe a inicialização quando um campo obrigatório está ausente ou inválido.

Ele complementa o uso geral de Pydantic em Python, mas é distribuído como pacote separado.

Criar o modelo de configuração

python -m pip install pydantic-settings
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    app_name: str = "Minha API"
    debug: bool = False
    database_url: str
    api_key: SecretStr
    workers: int = 2

    model_config = SettingsConfigDict(
        env_file=".env",
        env_prefix="APP_",
        extra="ignore",
    )

settings = Settings()

As variáveis esperadas são APP_DATABASE_URL, APP_API_KEY e assim por diante. Strings como false e 4 são convertidas para os tipos declarados; valores inválidos geram erro de validação.

Um .env local pode conter:

APP_DEBUG=false
APP_DATABASE_URL=postgresql://localhost/app
APP_API_KEY=valor-local
APP_WORKERS=4

Adicione .env ao .gitignore e versione .env.example apenas com nomes e valores não sensíveis. SecretStr reduz exposição acidental em representações, mas não criptografa nem substitui um gerenciador de segredos.

Separar configuração por ambiente

Prefira o mesmo modelo e diferentes fontes de valores. Evite classes ProductionSettings e DevelopmentSettings cheias de regras divergentes quando apenas os valores mudam. Em produção, injete variáveis pelo serviço de deploy ou cofre de segredos.

A documentação oficial de Pydantic Settings detalha ordem das fontes, nomes aninhados, CLI e customização.

Usar sem criar globais difíceis de testar

Em uma API, carregue uma vez em um ponto explícito e injete o objeto onde necessário. No FastAPI, Depends permite substituir a configuração em testes. Para testes unitários, instancie Settings com valores controlados ou use monkeypatch no ambiente antes da criação.

Checklist de segurança

  • Nunca registre o valor de tokens e senhas.
  • Falhe na inicialização se uma opção obrigatória estiver ausente.
  • Valide limites como quantidade de workers e timeouts.
  • Não dependa do diretório atual sem documentar onde .env é lido.
  • Mantenha configuração separada de dados de negócio.
  • Faça rotação de segredos fora do código.

Configuração tipada melhora mensagens de erro e torna as dependências operacionais visíveis. O benefício mais importante é descobrir uma implantação incorreta antes que ela processe requisições.

Entender a prioridade das fontes

Por padrão, valores passados ao construtor têm prioridade sobre variáveis de ambiente, que prevalecem sobre o arquivo dotenv e, depois, sobre arquivos de secrets. Os valores padrão do modelo entram por último. Essa ordem permite sobrescrever uma opção local em um teste sem alterar o ambiente:

settings_teste = Settings(
    database_url="sqlite://",
    api_key="chave-de-teste",
    workers=1,
)

Não presuma que .env substitui uma variável já exportada no processo. Em geral, a variável de ambiente vence. Isso é desejável em containers e pipelines, mas pode confundir durante depuração. Registre quais opções não sensíveis foram selecionadas e documente a origem esperada, nunca o conteúdo de credenciais.

O carregamento de dotenv não procura recursivamente diretórios pais. O caminho relativo depende do diretório de trabalho do processo. Em serviços, considere formar um caminho conhecido a partir da localização da aplicação ou deixar a plataforma fornecer todas as variáveis.

Campos aninhados e valores complexos

Listas, dicionários e submodelos podem vir de strings JSON. Para sobrescrever partes de uma estrutura por variáveis separadas, configure um delimitador:

from pydantic import BaseModel

class Banco(BaseModel):
    host: str
    port: int = 5432
    pool_size: int = 10

class Settings(BaseSettings):
    database: Banco

    model_config = SettingsConfigDict(
        env_prefix="APP_",
        env_nested_delimiter="__",
    )

Com essa configuração, APP_DATABASE__HOST=db.internal e APP_DATABASE__POOL_SIZE=20 alimentam campos internos. O nome exato e a sensibilidade a maiúsculas variam conforme configuração e sistema operacional, então padronize nomes e teste no mesmo tipo de ambiente usado em produção.

Para regras adicionais, use validadores do Pydantic. Um timeout pode exigir valor positivo; um ambiente de produção pode proibir debug=True. Mantenha validações determinísticas e sem chamadas de rede. A criação de settings deve relatar configuração inválida, não depender da disponibilidade momentânea de outro serviço.

Aliases, prefixos e nomes legados

env_prefix reduz colisões quando várias aplicações compartilham um ambiente. Aliases permitem integrar nomes existentes, mas distinguem alias de validação, serialização e nome de variável. Antes de adotar uma opção, confirme seu efeito na versão instalada de pydantic-settings e escreva um teste com o nome real.

Evite manter indefinidamente vários nomes para o mesmo campo. Durante uma migração, aceite o nome antigo de modo explícito, avise operadores e defina quando ele será removido. Configuração silenciosamente ambígua é difícil de auditar.

Para ambientes em que maiúsculas e minúsculas são distintas, trate a grafia como parte do contrato operacional. Não dependa de comportamento específico do Windows se o deploy roda em Linux.

Segredos e representação segura

SecretStr oculta o conteúdo em repr() e na serialização comum, mas o valor continua na memória e pode ser acessado com get_secret_value(). Passe-o apenas ao cliente que precisa da credencial:

cliente = ClienteExterno(
    token=settings.api_key.get_secret_value()
)

Não transforme o objeto inteiro em dicionário para logs. Uma lista explícita de campos públicos é mais segura. Arquivos de secrets montados por orquestradores podem ser usados como fonte, mas permissões, rotação e persistência pertencem à plataforma. Pydantic valida e entrega o valor; não administra seu ciclo de vida.

Se um segredo é opcional apenas em desenvolvimento, modele essa decisão com cuidado. Um campo str | None pode permitir que produção suba sem proteção. Muitas equipes preferem campo obrigatório e fornecem credencial fictícia somente em testes isolados.

Carregar uma vez sem esconder dependências

Construir settings repetidamente relê fontes e refaz validação. Em uma aplicação longa, carregue uma vez ou use cache em uma função de composição:

from functools import lru_cache

@lru_cache
def get_settings() -> Settings:
    return Settings()

Consumidores ainda devem receber Settings ou uma seção menor por parâmetro. Importar um singleton global em todos os módulos dificulta testes e torna dependências invisíveis. No teste, limpe o cache depois de alterar o ambiente ou, melhor, passe uma instância criada diretamente.

Bibliotecas reutilizáveis não deveriam ler variáveis de ambiente ao serem importadas. Receba opções do aplicativo hospedeiro. Isso evita efeitos colaterais e permite que dois clientes da mesma biblioteca usem configurações diferentes.

Erros de validação operacionais

Capture ValidationError no ponto de entrada para apresentar uma mensagem legível e encerrar com código diferente de zero. Não continue com configuração parcial. Ao formatar o erro, tenha cuidado: entradas inválidas podem conter dados sensíveis. Prefira nomes de campos e tipos esperados a despejar todo o valor recebido.

Teste pelo menos ausência de campos obrigatórios, conversões válidas, valores fora dos limites e precedência. Use monkeypatch.setenv() e monkeypatch.delenv() no pytest para isolar o ambiente, instanciando o modelo somente depois. O processo de testes ou a máquina do desenvolvedor pode conter variáveis inesperadas, portanto remova explicitamente as que influenciam o cenário.

Por fim, trate configuração como uma interface versionada entre código e operação. Renomear uma variável pode quebrar o deploy mesmo que todos os testes unitários passem. Documente nomes, tipos, obrigatoriedade, exemplos seguros e política de migração junto do projeto.