---
title: "Variáveis de Ambiente em Python: python-dotenv sem vazar segredo"
url: "https://python.dev.br/blog/python-dotenv-env-vars-config/"
markdown_url: "https://python.dev.br/blog/python-dotenv-env-vars-config.MD"
description: "Aprenda a organizar variáveis de ambiente em Python com python-dotenv, .env.example, validação e cuidados para não vazar segredos."
date: "2026-06-12"
author: "Equipe Python Brasil"
---

# 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.


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](/blog/pydantic-settings-configuracao-python/), [deploy de aplicação Python](/blog/deploy-aplicacao-python/), [segurança em aplicações Python](/blog/seguranca-em-aplicacoes-python/) e [HTTPX com timeouts e retries](/blog/httpx-timeouts-retries-python/). 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`:

```python
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:

```bash
python -m pip install python-dotenv
```

Uso mínimo:

```python
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:

```dotenv
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:

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

No `.gitignore`:

```gitignore
.env
.env.*
!.env.example
```

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

```dotenv
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:

```python
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:

```python
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:

```yaml
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:

```dockerignore
.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](/blog/pydantic-settings-configuracao-python/) costuma ser melhor:

```python
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`:

```python
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.

```python
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:

```python
# 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()
```

```python
# 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](/blog/httpx-timeouts-retries-python/) e o artigo sobre [Docker Compose com PostgreSQL local](/blog/python-docker-compose-postgres-ambiente-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.
