Python

Guia Completo de Variáveis de Ambiente em Python: python-dotenv e pydantic-settings

13 min de leitura

Guia Completo de Variáveis de Ambiente em Python: python-dotenv e pydantic-settings

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 . 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 (por convenção) e carrega cada linha como uma variável de ambiente. O formato é simples: . Quando

<h2>Por que Variáveis de Ambiente Importam</h2>

<p>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.</p>

<p>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 <code>os.environ</code>. No entanto, gerenciar dezenas de variáveis manualmente é tedioso e propenso a erros. É aí que entram as ferramentas: <strong>python-dotenv</strong> simplifica o carregamento a partir de arquivos locais, enquanto <strong>pydantic-settings</strong> adiciona validação tipada e estruturada.</p>

<h2>Entendendo python-dotenv</h2>

<h3>Como Funciona</h3>

<p>O <strong>python-dotenv</strong> lê um arquivo chamado <code>.env</code> (por convenção) e carrega cada linha como uma variável de ambiente. O formato é simples: <code>CHAVE=valor</code>. Quando você chama <code>load_dotenv()</code>, a biblioteca injeta essas variáveis no dicionário <code>os.environ</code>, tornando-as acessíveis em qualquer parte do código.</p>

<p>A ideia é clara: você cria um arquivo <code>.env</code> 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.</p>

<h3>Instalação e Exemplo Básico</h3>

<pre><code class="language-bash">pip install python-dotenv</code></pre>

<p>Crie um arquivo <code>.env</code> no raiz do projeto:</p>

<pre><code>DATABASE_URL=postgresql://user:password@localhost/mydb

API_KEY=seu_token_secreto_aqui

DEBUG=True

LOG_LEVEL=INFO</code></pre>

<p>Agora, no seu código Python:</p>

<pre><code class="language-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(&#039;DATABASE_URL&#039;)

api_key = os.getenv(&#039;API_KEY&#039;)

debug = os.getenv(&#039;DEBUG&#039;) # Nota: isso é uma string &#039;True&#039;, não booleano

print(f&quot;Database: {db_url}&quot;)

print(f&quot;API Key: {api_key}&quot;)

print(f&quot;Debug mode: {debug}&quot;)</code></pre>

<h3>Melhorando com Valores Padrão e Caminho Customizado</h3>

<p>O método <code>os.getenv()</code> aceita um segundo argumento como valor padrão, útil quando uma variável é opcional:</p>

<pre><code class="language-python">from dotenv import load_dotenv

import os

Carregar de um arquivo específico (não obrigatório ser .env)

load_dotenv(dotenv_path=&#039;config/.env.local&#039;)

Com valor padrão

timeout = os.getenv(&#039;REQUEST_TIMEOUT&#039;, &#039;30&#039;)

redis_host = os.getenv(&#039;REDIS_HOST&#039;, &#039;localhost&#039;)

print(f&quot;Timeout: {timeout}&quot;)

print(f&quot;Redis Host: {redis_host}&quot;)</code></pre>

<p>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 <strong>pydantic-settings</strong>.</p>

<h2>Estrutura Profissional com pydantic-settings</h2>

<h3>Por Que Usar Pydantic-Settings</h3>

<p><strong>pydantic-settings</strong> 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.</p>

<p>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.</p>

<h3>Instalação e Primeiro Exemplo</h3>

<pre><code class="language-bash">pip install pydantic-settings pydantic</code></pre>

<p>Crie um arquivo <code>.env</code>:</p>

<pre><code>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</code></pre>

<p>Agora, defina suas configurações em <code>config.py</code>:</p>

<pre><code class="language-python">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 = &quot;chave_padrao&quot;

debug: bool = False

Campo com alias (lê DATABASE_POOL_SIZE do .env)

database_pool_size: int = Field(default=5, alias=&#039;DATABASE_POOL_SIZE&#039;)

redis_enabled: bool = True

log_level: str = &quot;WARNING&quot;

max_workers: int = 2

class Config:

Arquivo .env a ser lido

env_file = &#039;.env&#039;

env_file_encoding = &#039;utf-8&#039;

Ignora variáveis de ambiente não declaradas

extra = &#039;ignore&#039;

Instancia uma única vez (padrão singleton é comum)

settings = Settings()</code></pre>

<p>Use-o em qualquer lugar do código:</p>

<pre><code class="language-python">from config import settings

print(f&quot;Database: {settings.database_url}&quot;)

print(f&quot;Pool size: {settings.database_pool_size}&quot;) # Já é int

print(f&quot;Debug: {settings.debug}&quot;) # Já é bool

print(f&quot;Workers: {settings.max_workers}&quot;) # Já é int

Operações seguras

total_connections = settings.database_pool_size * 2</code></pre>

<h3>Validação e Tratamento de Erros</h3>

<p>A força do pydantic aparece quando há erros. Se uma variável obrigatória estiver faltando ou tem tipo errado:</p>

<pre><code class="language-python"># .env com erro

DEBUG=not_a_boolean

DATABASE_POOL_SIZE=abc

from config import Settings

try:

settings = Settings()

except Exception as e:

print(f&quot;Erro ao carregar configurações: {e}&quot;)</code></pre>

<p>O pydantic lança um erro descritivo listando exatamente qual campo falhou e por quê. Isso economiza horas de debug.</p>

<h3>Separar Ambientes (Desenvolvimento, Teste, Produção)</h3>

<p>Uma abordagem profissional é ter múltiplos arquivos <code>.env</code>:</p>

<pre><code>.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</code></pre>

<p>Carregue dinamicamente:</p>

<pre><code class="language-python">import os

from pydantic_settings import BaseSettings

from dotenv import load_dotenv

Define qual ambiente está ativo

ENVIRONMENT = os.getenv(&#039;ENVIRONMENT&#039;, &#039;development&#039;)

Carrega o arquivo apropriado

load_dotenv(f&#039;.env.{ENVIRONMENT}&#039;)

class Settings(BaseSettings):

database_url: str

api_key: str

debug: bool = False

class Config:

env_file = f&#039;.env.{ENVIRONMENT}&#039;

env_file_encoding = &#039;utf-8&#039;

settings = Settings()</code></pre>

<p>Execute com: <code>ENVIRONMENT=production python seu_app.py</code></p>

<h2>Padrões Avançados e Boas Práticas</h2>

<h3>Variáveis Aninhadas e Tipos Complexos</h3>

<p>Pydantic permite estruturas mais sofisticadas. Por exemplo, se você tem várias instâncias de banco de dados:</p>

<pre><code class="language-python">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 = &quot;INFO&quot;

class Config:

env_file = &#039;.env&#039;

Permite parsing de variáveis com prefixo

env_nested_delimiter = &#039;__&#039;

settings = Settings()</code></pre>

<p>Com arquivo <code>.env</code>:</p>

<pre><code>PRIMARY_DB__URL=postgresql://localhost/main

PRIMARY_DB__POOL_SIZE=20

REPLICA_DB__URL=postgresql://replica-host/main

REPLICA_DB__POOL_SIZE=10</code></pre>

<h3>Validação Customizada</h3>

<p>Você pode adicionar validadores que rodam automaticamente:</p>

<pre><code class="language-python">from pydantic import field_validator

from pydantic_settings import BaseSettings

class Settings(BaseSettings):

api_key: str

max_workers: int

@field_validator(&#039;api_key&#039;)

@classmethod

def validate_api_key(cls, v):

if len(v) &lt; 10:

raise ValueError(&#039;API key deve ter no mínimo 10 caracteres&#039;)

return v

@field_validator(&#039;max_workers&#039;)

@classmethod

def validate_workers(cls, v):

if v &lt; 1 or v &gt; 100:

raise ValueError(&#039;max_workers deve estar entre 1 e 100&#039;)

return v

class Config:

env_file = &#039;.env&#039;

settings = Settings()</code></pre>

<h3>Logging de Configurações (com Segurança)</h3>

<p>Ao debugar, você quer ver quais valores foram carregados, mas nunca deve logar senhas:</p>

<pre><code class="language-python">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 = &#039;.env&#039;

def log_loaded_config(self):

&quot;&quot;&quot;Log seguro das configurações carregadas&quot;&quot;&quot;

logger.info(f&quot;Debug mode: {self.debug}&quot;)

logger.info(f&quot;Database host: {self.database_url.split(&#039;@&#039;)[1] if &#039;@&#039; in self.database_url else &#039;unknown&#039;}&quot;)

Nunca loga api_key inteira

logger.info(f&quot;API key loaded: {&#039;&#039; len(self.api_key) if self.api_key else &#039;None&#039;}&quot;)

settings = Settings()

settings.log_loaded_config()</code></pre>

<h2>Comparação: python-dotenv vs pydantic-settings</h2>

<div class="table-wrap"><table><thead><tr><th>Aspecto</th><th>python-dotenv</th><th>pydantic-settings</th></tr></thead><tbody><tr><td><strong>Complexidade</strong></td><td>Simples</td><td>Moderada</td></tr><tr><td><strong>Validação de Tipos</strong></td><td>Nenhuma</td><td>Rigorosa</td></tr><tr><td><strong>Casting Automático</strong></td><td>Não</td><td>Sim</td></tr><tr><td><strong>Estruturas Complexas</strong></td><td>Não suporta</td><td>Suporta</td></tr><tr><td><strong>Curva de Aprendizado</strong></td><td>Muito baixa</td><td>Média</td></tr><tr><td><strong>Projeto Pequeno</strong></td><td>Perfeito</td><td>Overkill</td></tr><tr><td><strong>Projeto Corporativo</strong></td><td>Insuficiente</td><td>Ideal</td></tr></tbody></table></div>

<p>Use <strong>python-dotenv</strong> se você tem poucos parâmetros e não precisa de validação. Use <strong>pydantic-settings</strong> se sua aplicação cresce ou trabalha em equipe.</p>

<h2>Conclusão</h2>

<p>Aprendemos que variáveis de ambiente são essenciais para separar configuração de código. <strong>python-dotenv</strong> oferece uma forma simples e direta de carregar pares chave-valor de um arquivo <code>.env</code>, ideal para prototipagem. <strong>pydantic-settings</strong> 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.</p>

<p>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.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://github.com/theskumar/python-dotenv" target="_blank" rel="noopener noreferrer">python-dotenv - Documentação Oficial</a></li>

<li><a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/" target="_blank" rel="noopener noreferrer">Pydantic Settings - Documentação Oficial</a></li>

<li><a href="https://12factor.net/config" target="_blank" rel="noopener noreferrer">12 Factor App - Configuration</a></li>

<li><a href="https://realpython.com/python-dotenv/" target="_blank" rel="noopener noreferrer">Real Python - Environment Variables in Python</a></li>

<li><a href="https://fastapi.tiangolo.com/advanced/settings/" target="_blank" rel="noopener noreferrer">FastAPI Docs - Environment Variables</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

Como Usar Introdução ao Python: Filosofia, Instalação, pyenv e Primeiro Script em Produção
Como Usar Introdução ao Python: Filosofia, Instalação, pyenv e Primeiro Script em Produção

A Filosofia do Python Python é uma linguagem de programação nascida em 1989,...

O que Todo Dev Deve Saber sobre httpx e aiohttp em Python: Requisições HTTP Assíncronas
O que Todo Dev Deve Saber sobre httpx e aiohttp em Python: Requisições HTTP Assíncronas

Entendendo Programação Assíncrona em Python Antes de mergulharmos em e , prec...

Guia Completo de Observabilidade em Python: OpenTelemetry, Sentry e Profiling com py-spy
Guia Completo de Observabilidade em Python: OpenTelemetry, Sentry e Profiling com py-spy

O Que é Observabilidade e Por Que Você Precisa Disso Observabilidade é a capa...