Variáveis de Ambiente em Python: python-dotenv sem vazar segredo

Aprenda a organizar variáveis de ambiente em Python com python-dotenv, .env.example, validação e cuidados para não vazar segredos.

8 min de leitura Equipe Python Brasil

Todo projeto Python que passa de um script local para uma rotina real encontra a mesma pergunta: onde ficam tokens, senhas, URLs de banco, chaves de API e flags de ambiente? Colocar tudo direto no código parece rápido no começo, mas vira dívida técnica imediatamente. O arquivo vai para o Git, alguém copia o exemplo para produção, o token aparece em log ou o deploy quebra porque a configuração que funcionava na sua máquina não existe no servidor.

A resposta profissional não é espalhar os.environ sem critério nem criar um .env mágico que ninguém entende. A resposta é tratar configuração como parte do projeto: nomes claros, valores obrigatórios validados, arquivo de exemplo versionado, segredos reais fora do repositório e uma fronteira explícita entre desenvolvimento local e produção.

Neste guia, vamos usar python-dotenv como ferramenta simples para desenvolvimento local, mas com a disciplina necessária para não vazar segredo. O conteúdo conversa com Pydantic Settings, deploy de aplicação Python, segurança em aplicações Python e HTTPX com timeouts e retries. O foco é prático: deixar seu projeto fácil de rodar, revisar e publicar.

O que são variáveis de ambiente

Variável de ambiente é uma configuração fornecida pelo sistema operacional para o processo. Em vez de escrever a URL do banco dentro do código, você lê DATABASE_URL. Em vez de commitar um token, você lê API_TOKEN. Assim, o mesmo código pode rodar no notebook local, em um container Docker, em um servidor, em uma plataforma serverless ou em uma esteira de CI sem precisar de branches diferentes.

Em Python puro, a leitura é feita com os.environ:

import os

api_url = os.environ["API_URL"]
timeout = float(os.environ.get("API_TIMEOUT", "5"))

A diferença entre os.environ["API_URL"] e os.environ.get("API_URL") é importante. A primeira forma falha rápido se a variável não existir. A segunda retorna None ou um valor padrão. Para configurações obrigatórias, falhar rápido costuma ser melhor: você descobre o problema no início do processo, não depois de processar metade de um lote.

Onde o python-dotenv entra

Em produção, variáveis de ambiente normalmente vêm do orquestrador, do painel da hospedagem, do Kubernetes, do systemd, do GitHub Actions, do Gitea Actions ou de um gerenciador de segredos. No desenvolvimento local, ninguém quer digitar vinte exports antes de rodar o projeto. É aí que python-dotenv ajuda: ele lê um arquivo .env e injeta os valores no ambiente do processo.

Instalação:

python -m pip install python-dotenv

Uso mínimo:

from dotenv import load_dotenv

load_dotenv()

Por padrão, load_dotenv() procura um arquivo .env no diretório atual ou em diretórios acima. Depois disso, seu código pode continuar usando os.environ, sem depender diretamente do formato do arquivo.

Um .env local pode ser assim:

API_URL=https://api.exemplo.com
API_TIMEOUT=5
DATABASE_URL=postgresql://usuario:senha@localhost:5432/app
DEBUG=true

Esse arquivo é conveniente, mas também perigoso: ele pode conter segredo real. Por isso, a regra principal é simples: .env entra no .gitignore; .env.example entra no Git.

A estrutura mínima segura

Um projeto pequeno pode começar com estes arquivos:

meu-projeto/
  app/
    __init__.py
    config.py
    main.py
  .env
  .env.example
  .gitignore
  pyproject.toml

No .gitignore:

.env
.env.*
!.env.example

No .env.example, coloque nomes e formatos, nunca segredos reais:

API_URL=https://api.exemplo.com
API_TIMEOUT=5
DATABASE_URL=postgresql://usuario:senha@localhost:5432/app
DEBUG=false

Esse arquivo serve como contrato para quem clona o repositório. Se uma variável nova é necessária, ela deve aparecer no .env.example no mesmo commit em que o código começa a usá-la.

Em app/config.py, centralize a leitura:

from dataclasses import dataclass
import os

from dotenv import load_dotenv

load_dotenv()

@dataclass(frozen=True)
class Settings:
    api_url: str
    api_timeout: float
    database_url: str
    debug: bool


def _required(name: str) -> str:
    value = os.environ.get(name)
    if not value:
        raise RuntimeError(f"Variável obrigatória ausente: {name}")
    return value


def _bool(name: str, default: str = "false") -> bool:
    return os.environ.get(name, default).lower() in {"1", "true", "yes", "sim"}


def load_settings() -> Settings:
    return Settings(
        api_url=_required("API_URL"),
        api_timeout=float(os.environ.get("API_TIMEOUT", "5")),
        database_url=_required("DATABASE_URL"),
        debug=_bool("DEBUG"),
    )

O restante da aplicação importa load_settings() e não precisa saber se a configuração veio de .env, container, CI ou servidor.

Não chame load_dotenv em todo lugar

Um erro comum é colocar load_dotenv() em vários arquivos: main.py, database.py, client.py, tasks.py. Isso cria dependência escondida e dificulta testes. Prefira uma fronteira única, geralmente no módulo de configuração ou no ponto de entrada da aplicação.

Para scripts, uma estrutura simples funciona:

from app.config import load_settings
from app.cliente import ClienteAPI


def main() -> None:
    settings = load_settings()
    cliente = ClienteAPI(base_url=settings.api_url, timeout=settings.api_timeout)
    cliente.executar()


if __name__ == "__main__":
    main()

Para aplicações FastAPI, carregue as configurações ao criar a aplicação e injete onde precisar. Para jobs de fila, carregue no início do worker. Para testes, use monkeypatch ou variáveis temporárias em vez de depender de um .env real.

Produção não deve depender do seu .env local

python-dotenv é ótimo para desenvolvimento, mas produção precisa de fonte de verdade. Em Docker Compose, você pode usar env_file para ambiente local e variáveis reais no deploy:

services:
  app:
    build: .
    env_file:
      - .env
    command: python -m app.main

Em Kubernetes, prefira Secret e ConfigMap. Em plataformas gerenciadas, use o painel de environment variables. Em CI/CD, use secrets da plataforma. O princípio é: o código lê variáveis; cada ambiente decide como fornecê-las.

Nunca copie .env para dentro da imagem Docker com COPY . . sem cuidado. Se o arquivo não estiver no .dockerignore, ele pode acabar dentro da imagem e ser distribuído junto com a aplicação. Inclua:

.env
.env.*
!.env.example

Esse detalhe evita um vazamento comum em times pequenos: o .env não está no Git, mas foi parar na imagem enviada para o registry.

Validação: quando usar Pydantic Settings

A versão com dataclass funciona para projetos pequenos, mas cresce mal quando há muitos tipos, listas, URLs, enums e validação condicional. Nesse ponto, Pydantic Settings costuma ser melhor:

from pydantic import AnyUrl, Field
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")

    api_url: AnyUrl
    api_timeout: float = Field(default=5, ge=0.1, le=60)
    database_url: str
    debug: bool = False

Essa abordagem valida tipos, gera mensagens de erro melhores e reduz conversões manuais. Mesmo assim, a regra de segurança continua igual: .env é local e ignorado; .env.example é o contrato público.

Cuidados para não vazar segredo

A maior parte dos vazamentos não acontece por ataque sofisticado. Acontece por hábito ruim. Alguns controles simples reduzem muito o risco:

  1. Nunca escreva segredo real no README. Use sk-..., troque-este-valor ou exemplos obviamente falsos.
  2. Não faça print de configuração inteira. Se precisar debugar, mostre apenas nomes de variáveis ou valores mascarados.
  3. Não capture .env em screenshots. Parece banal, mas é recorrente em tutoriais e issues.
  4. Não reaproveite o mesmo token para dev e produção. Se vazar no ambiente local, o dano deve ser limitado.
  5. Revogue tokens depois de exposição acidental. Remover do Git não basta; o histórico continua existindo.
  6. Use scanners de segredo no CI. Ferramentas como gitleaks ajudam a bloquear commits perigosos.

Também vale tomar cuidado com logs estruturados. Um erro de validação que imprime o dicionário inteiro de configuração pode mandar DATABASE_URL para o agregador de logs. Prefira representar campos sensíveis como ***.

Testando configuração sem depender do ambiente real

Teste bom não exige seu .env local. Com pytest, você pode usar monkeypatch:

from app.config import load_settings


def test_load_settings(monkeypatch):
    monkeypatch.setenv("API_URL", "https://api.exemplo.com")
    monkeypatch.setenv("API_TIMEOUT", "3")
    monkeypatch.setenv("DATABASE_URL", "postgresql://local/teste")
    monkeypatch.setenv("DEBUG", "true")

    settings = load_settings()

    assert settings.api_url == "https://api.exemplo.com"
    assert settings.api_timeout == 3
    assert settings.debug is True

Para testar variável obrigatória ausente, remova o valor e verifique a exceção. Isso evita deploys que só falham depois de subir.

import pytest

from app.config import load_settings


def test_database_url_obrigatoria(monkeypatch):
    monkeypatch.setenv("API_URL", "https://api.exemplo.com")
    monkeypatch.delenv("DATABASE_URL", raising=False)

    with pytest.raises(RuntimeError, match="DATABASE_URL"):
        load_settings()

Se você usa Pydantic Settings, isole cache e instâncias globais durante testes. Configuração carregada no import pode deixar testes frágeis porque uma suíte influencia a outra.

Checklist para revisar pull requests

Antes de aprovar um PR que mexe em configuração, revise estes pontos:

  • A nova variável está documentada no .env.example?
  • O .env real continua ignorado pelo Git?
  • A aplicação falha com mensagem clara quando falta configuração obrigatória?
  • Valores numéricos e booleanos são convertidos explicitamente?
  • Segredos não aparecem em README, teste, fixture, log ou screenshot?
  • Dockerfile e .dockerignore impedem que .env vá para a imagem?
  • O CI recebe variáveis por secrets, não por arquivo versionado?
  • O deploy tem nomes iguais aos usados no código?

Esse checklist parece burocrático, mas economiza horas. Muitos incidentes de produção são apenas diferença entre API_KEY, API_TOKEN e TOKEN em ambientes diferentes.

Exemplo completo de cliente configurado

Juntando configuração e cliente HTTP, você pode criar uma integração previsível:

# app/client.py
import httpx

class ClientePedidos:
    def __init__(self, base_url: str, token: str, timeout: float) -> None:
        self._client = httpx.Client(
            base_url=base_url,
            timeout=timeout,
            headers={"Authorization": f"Bearer {token}"},
        )

    def listar_pedidos(self) -> list[dict]:
        response = self._client.get("/pedidos")
        response.raise_for_status()
        return response.json()
# app/main.py
from app.client import ClientePedidos
from app.config import load_settings


def main() -> None:
    settings = load_settings()
    cliente = ClientePedidos(
        base_url=settings.api_url,
        token=settings.api_token,
        timeout=settings.api_timeout,
    )
    pedidos = cliente.listar_pedidos()
    print(f"Pedidos recebidos: {len(pedidos)}")

Nesse exemplo, ainda faltaria adicionar api_token ao Settings, ao .env.example e aos secrets de produção. Essa repetição é intencional: configuração importante deve aparecer nos pontos onde será revisada.

Conclusão

python-dotenv não é arquitetura de produção, mas é uma ótima ponte entre o notebook local e uma aplicação organizada. Use a biblioteca para tornar o desenvolvimento simples, não para esconder decisões. O ganho real vem do conjunto: nomes consistentes, validação clara, .env.example versionado, .env ignorado, produção abastecida por secrets e testes que montam o ambiente de forma explícita.

Se você está começando, crie hoje um config.py centralizado e pare de ler variáveis em arquivos aleatórios. Se o projeto já cresceu, migre para Pydantic Settings e adicione validação de tipos. Em ambos os casos, trate segredo como segredo: nunca confie que “só esse commit” ou “só esse print” não vai escapar.

Para continuar, leia também o guia de HTTPX com timeouts e retries e o artigo sobre Docker Compose com PostgreSQL local. Configuração bem feita é a base para integrações confiáveis, deploys tranquilos e automações Python que sobrevivem fora da sua máquina.

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português