Por que Variáveis de Ambiente Importam
Variáveis de ambiente são valores configuráveis que seu programa lê do sistema operacional, sem que precisem estar hardcoded no código-fonte. Isso é fundamental para segurança, portabilidade e manutenção. Imagine que sua aplicação precisa conectar a um banco de dados: você não quer que a senha apareça no repositório Git. Da mesma forma, URLs de APIs, chaves de autenticação e configurações específicas de cada ambiente (desenvolvimento, teste, produção) mudam constantemente.
A maioria das linguagens modernas oferece acesso direto a variáveis de ambiente através de bibliotecas padrão. Em Python, você pode acessá-las via os.environ. No entanto, gerenciar dezenas de variáveis manualmente é tedioso e propenso a erros. É aí que entram as ferramentas: python-dotenv simplifica o carregamento a partir de arquivos locais, enquanto pydantic-settings adiciona validação tipada e estruturada.
Entendendo python-dotenv
Como Funciona
O python-dotenv lê um arquivo chamado .env (por convenção) e carrega cada linha como uma variável de ambiente. O formato é simples: CHAVE=valor. Quando você chama load_dotenv(), a biblioteca injeta essas variáveis no dicionário os.environ, tornando-as acessíveis em qualquer parte do código.
A ideia é clara: você cria um arquivo .env localmente (nunca commitado no Git), o desenvolvimento funciona naturalmente, e em produção as variáveis vêm do sistema operacional ou de um gerenciador de secrets.
Instalação e Exemplo Básico
pip install python-dotenv
Crie um arquivo .env no raiz do projeto:
DATABASE_URL=postgresql://user:password@localhost/mydb
API_KEY=seu_token_secreto_aqui
DEBUG=True
LOG_LEVEL=INFO
Agora, no seu código Python:
import os
from dotenv import load_dotenv
# Carrega as variáveis do arquivo .env
load_dotenv()
# Acessa as variáveis como strings
db_url = os.getenv('DATABASE_URL')
api_key = os.getenv('API_KEY')
debug = os.getenv('DEBUG') # Nota: isso é uma string 'True', não booleano
print(f"Database: {db_url}")
print(f"API Key: {api_key}")
print(f"Debug mode: {debug}")
Melhorando com Valores Padrão e Caminho Customizado
O método os.getenv() aceita um segundo argumento como valor padrão, útil quando uma variável é opcional:
from dotenv import load_dotenv
import os
# Carregar de um arquivo específico (não obrigatório ser .env)
load_dotenv(dotenv_path='config/.env.local')
# Com valor padrão
timeout = os.getenv('REQUEST_TIMEOUT', '30')
redis_host = os.getenv('REDIS_HOST', 'localhost')
print(f"Timeout: {timeout}")
print(f"Redis Host: {redis_host}")
A limitação do python-dotenv é que tudo vem como string. Se você precisa de um inteiro ou booleano, deve fazer casting manual. Aqui entra o pydantic-settings.
Estrutura Profissional com pydantic-settings
Por Que Usar Pydantic-Settings
pydantic-settings combina o carregamento de variáveis de ambiente com validação de tipos rigorosa. Você define uma classe que descreve a forma de suas configurações — quais campos são obrigatórios, seus tipos exatos, valores padrão — e a biblioteca se encarrega do resto: lê as variáveis, converte para os tipos corretos, valida e lança exceções se algo estiver errado.
Isso evita bugs silenciosos como tentar fazer operações matemáticas em uma string quando esperava um inteiro, ou deixar passar uma URL inválida.
Instalação e Primeiro Exemplo
pip install pydantic-settings pydantic
Crie um arquivo .env:
DATABASE_URL=postgresql://user:senha@localhost/banco
DATABASE_POOL_SIZE=10
API_KEY=chave_super_secreta
DEBUG=false
REDIS_ENABLED=true
LOG_LEVEL=INFO
MAX_WORKERS=4
Agora, defina suas configurações em config.py:
from pydantic_settings import BaseSettings
from pydantic import Field
class Settings(BaseSettings):
# Campo obrigatório
database_url: str
# Com valores padrão
api_key: str = "chave_padrao"
debug: bool = False
# Campo com alias (lê DATABASE_POOL_SIZE do .env)
database_pool_size: int = Field(default=5, alias='DATABASE_POOL_SIZE')
redis_enabled: bool = True
log_level: str = "WARNING"
max_workers: int = 2
class Config:
# Arquivo .env a ser lido
env_file = '.env'
env_file_encoding = 'utf-8'
# Ignora variáveis de ambiente não declaradas
extra = 'ignore'
# Instancia uma única vez (padrão singleton é comum)
settings = Settings()
Use-o em qualquer lugar do código:
from config import settings
print(f"Database: {settings.database_url}")
print(f"Pool size: {settings.database_pool_size}") # Já é int
print(f"Debug: {settings.debug}") # Já é bool
print(f"Workers: {settings.max_workers}") # Já é int
# Operações seguras
total_connections = settings.database_pool_size * 2
Validação e Tratamento de Erros
A força do pydantic aparece quando há erros. Se uma variável obrigatória estiver faltando ou tem tipo errado:
# .env com erro
# DEBUG=not_a_boolean
# DATABASE_POOL_SIZE=abc
from config import Settings
try:
settings = Settings()
except Exception as e:
print(f"Erro ao carregar configurações: {e}")
O pydantic lança um erro descritivo listando exatamente qual campo falhou e por quê. Isso economiza horas de debug.
Separar Ambientes (Desenvolvimento, Teste, Produção)
Uma abordagem profissional é ter múltiplos arquivos .env:
.env.example # Versionado no Git (template)
.env.development # Local, não versionado
.env.test # Para testes, pode ser versionado
.env.production # Em produção, variáveis vêm do sistema
Carregue dinamicamente:
import os
from pydantic_settings import BaseSettings
from dotenv import load_dotenv
# Define qual ambiente está ativo
ENVIRONMENT = os.getenv('ENVIRONMENT', 'development')
# Carrega o arquivo apropriado
load_dotenv(f'.env.{ENVIRONMENT}')
class Settings(BaseSettings):
database_url: str
api_key: str
debug: bool = False
class Config:
env_file = f'.env.{ENVIRONMENT}'
env_file_encoding = 'utf-8'
settings = Settings()
Execute com: ENVIRONMENT=production python seu_app.py
Padrões Avançados e Boas Práticas
Variáveis Aninhadas e Tipos Complexos
Pydantic permite estruturas mais sofisticadas. Por exemplo, se você tem várias instâncias de banco de dados:
from pydantic_settings import BaseSettings
from pydantic import BaseModel
class DatabaseConfig(BaseModel):
url: str
pool_size: int = 5
timeout: int = 30
class Settings(BaseSettings):
primary_db: DatabaseConfig
replica_db: DatabaseConfig
log_level: str = "INFO"
class Config:
env_file = '.env'
# Permite parsing de variáveis com prefixo
env_nested_delimiter = '__'
settings = Settings()
Com arquivo .env:
PRIMARY_DB__URL=postgresql://localhost/main
PRIMARY_DB__POOL_SIZE=20
REPLICA_DB__URL=postgresql://replica-host/main
REPLICA_DB__POOL_SIZE=10
Validação Customizada
Você pode adicionar validadores que rodam automaticamente:
from pydantic import field_validator
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
api_key: str
max_workers: int
@field_validator('api_key')
@classmethod
def validate_api_key(cls, v):
if len(v) < 10:
raise ValueError('API key deve ter no mínimo 10 caracteres')
return v
@field_validator('max_workers')
@classmethod
def validate_workers(cls, v):
if v < 1 or v > 100:
raise ValueError('max_workers deve estar entre 1 e 100')
return v
class Config:
env_file = '.env'
settings = Settings()
Logging de Configurações (com Segurança)
Ao debugar, você quer ver quais valores foram carregados, mas nunca deve logar senhas:
import logging
from pydantic_settings import BaseSettings
logger = logging.getLogger(__name__)
class Settings(BaseSettings):
database_url: str
api_key: str
debug: bool = False
class Config:
env_file = '.env'
def log_loaded_config(self):
"""Log seguro das configurações carregadas"""
logger.info(f"Debug mode: {self.debug}")
logger.info(f"Database host: {self.database_url.split('@')[1] if '@' in self.database_url else 'unknown'}")
# Nunca loga api_key inteira
logger.info(f"API key loaded: {'*' * len(self.api_key) if self.api_key else 'None'}")
settings = Settings()
settings.log_loaded_config()
Comparação: python-dotenv vs pydantic-settings
| Aspecto | python-dotenv | pydantic-settings |
|---|---|---|
| Complexidade | Simples | Moderada |
| Validação de Tipos | Nenhuma | Rigorosa |
| Casting Automático | Não | Sim |
| Estruturas Complexas | Não suporta | Suporta |
| Curva de Aprendizado | Muito baixa | Média |
| Projeto Pequeno | Perfeito | Overkill |
| Projeto Corporativo | Insuficiente | Ideal |
Use python-dotenv se você tem poucos parâmetros e não precisa de validação. Use pydantic-settings se sua aplicação cresce ou trabalha em equipe.
Conclusão
Aprendemos que variáveis de ambiente são essenciais para separar configuração de código. python-dotenv oferece uma forma simples e direta de carregar pares chave-valor de um arquivo .env, ideal para prototipagem. pydantic-settings eleva isso a um nível profissional, adicionando validação tipada, tratamento de erros robusto e suporte a estruturas complexas — é o padrão em aplicações sérias e equipes.
A escolha não é binária: você pode começar com python-dotenv e evoluir para pydantic-settings conforme o projeto cresce. O importante é nunca hardcodear secrets ou configurações dependentes de ambiente diretamente no código.