Virtual environment, pip e pyproject.toml em Python

1. Visão geral

Quando você trabalha com vários projetos Python na mesma máquina, logo enfrenta conflitos de versão. O projeto A precisa do requests 2.28, enquanto o projeto B precisa do requests 2.32. Este artigo aborda três ferramentas para resolver esse problema. venv isola o ambiente para cada projeto. pip instala e gerencia pacotes. pyproject.toml declara o projeto de acordo com o padrão moderno. Depois de ler, você será capaz de criar um projeto Python com estrutura padrão, instalar no modo editável e executar testes com pytest.

2. Virtual environment

Um virtual environment (venv) é um diretório contendo três coisas: um interpretador Python próprio (geralmente um symlink), um diretório site-packages próprio e scripts de ativação. Os pacotes que você instala no venv residem apenas nesse diretório, sem afetar o Python do sistema.

2.1. Criando e ativando um venv

O módulo venv vem na biblioteca padrão desde o Python 3.3, então você não precisa instalar nada extra:

mkdir shopcart && cd shopcart
python3 -m venv .venv

# Linux / macOS
source .venv/bin/activate

# Windows (PowerShell)
.venv\Scripts\Activate.ps1

O comando python3 -m venv .venv cria o diretório .venv no seu projeto. Nomear como .venv é uma convenção comum, e a maioria das IDEs o reconhecerá automaticamente. Depois de ativar, o prompt do shell terá o prefixo (.venv). Os comandos python e pip agora apontam para o venv.

Para sair do venv, você executa:

deactivate

2.2. Verificando qual interpreter você está usando

Esse é um passo que muitos pulam, e é a origem de muitos erros do tipo “já instalei mas ainda recebo ModuleNotFoundError”:

which python
python -c "import sys; print(sys.prefix); print(sys.prefix != sys.base_prefix)"
/home/lan/shopcart/.venv/bin/python
/home/lan/shopcart/.venv
True

sys.prefix é o diretório raiz do ambiente em execução. sys.base_prefix é o diretório do Python original. Valores diferentes significam que você está dentro de um venv.

Nota: Não faça commit do diretório .venv no Git. Adicione .venv/ ao .gitignore. O venv depende de caminhos absolutos e do sistema operacional, então copiar para outra máquina geralmente quebra. A forma correta é fazer commit do arquivo de declaração de dependências e recriar o venv na nova máquina.

3. Gerenciando pacotes com pip

pip é o instalador de pacotes padrão, obtendo pacotes do PyPI.

3.1. Comandos básicos

python -m pip install --upgrade pip
python -m pip install "requests>=2.31,<3"
python -m pip list
python -m pip show requests
python -m pip uninstall -y requests

Cada comando faz o seguinte:

  • install --upgrade pip atualiza o próprio pip. Versões antigas do pip às vezes não leem pyproject.toml corretamente.
  • "requests>=2.31,<3" é um especificador de versão. Você precisa colocá-lo entre aspas para o shell não interpretar < como redirecionamento.
  • list lista os pacotes instalados. show imprime informações detalhadas, incluindo versão, local de instalação e pacotes dos quais depende.

3.2. Por que usar python -m pip em vez de pip

O comando pip sozinho pode apontar para um Python diferente do que você está usando. Isso geralmente acontece quando a máquina tem múltiplas versões do Python ou quando você esquece de ativar o venv. O comando python -m pip sempre executa pip do interpretador correto, então o pacote é certamente instalado no ambiente desejado.

3.3. requirements.txt e pip freeze

A forma tradicional de compartilhar uma lista de pacotes é usar requirements.txt:

python -m pip freeze > requirements.txt
python -m pip install -r requirements.txt
certifi==2024.8.30
charset-normalizer==3.4.0
idna==3.10
requests==2.32.3
urllib3==2.2.3

pip freeze registra todos os pacotes no venv com as versões exatas, incluindo dependências indiretas como urllib3. Essa abordagem é adequada para reproduzir um ambiente de execução. No entanto, não diz quais pacotes você realmente precisa e quais são apenas puxados como dependências. Também não descreve seu projeto: nome, versão, requisitos do Python. Isso é o trabalho de pyproject.toml.

4. Declarando um projeto com pyproject.toml

pyproject.toml é o arquivo de configuração padrão para um projeto Python. É definido nos padrões PEP 518, PEP 621 e PEPs relacionados. Este arquivo substitui setup.py, setup.cfg e consolida a configuração de várias ferramentas como pytest ou ruff em um único lugar.

4.1. Estrutura do projeto

Usamos src layout, ou seja, colocamos o código em src/. Esse layout garante que os testes sempre executem no pacote instalado, não importando acidentalmente do diretório de trabalho:

shopcart/
├── pyproject.toml
├── src/
│   └── shopcart/
│       ├── __init__.py
│       ├── order.py
│       └── cli.py
└── tests/
    └── test_order.py

4.2. Arquivo pyproject.toml completo

[build-system]
requires = ["setuptools>=69"]
build-backend = "setuptools.build_meta"

[project]
name = "shopcart"
version = "0.1.0"
description = "Calcular o total de um pedido simples"
requires-python = ">=3.11"
dependencies = [
    "requests>=2.31,<3",
]

[project.optional-dependencies]
dev = [
    "pytest>=8",
    "ruff>=0.5",
]

[project.scripts]
shopcart = "shopcart.cli:main"

[tool.pytest.ini_options]
testpaths = ["tests"]

O significado de cada seção:

  • [build-system] diz ao pip quais ferramentas são necessárias para construir o projeto. Aqui é setuptools. Você pode substituir por hatchling ou flit_core sem alterar o resto.
  • [project] contém metadados padrão. requires-python impede que outras pessoas instalem o projeto em versões não suportadas do Python.
  • dependencies são os pacotes obrigatórios para executar o projeto. Aqui você deve declarar intervalos de versão (>=2.31,<3), não versões fixas (==2.32.3).
  • [project.optional-dependencies] agrupa pacotes opcionais. O grupo dev contém ferramentas de desenvolvimento que usuários finais não precisam.
  • [project.scripts] cria o comando shopcart no terminal. Esse comando chama a função main do módulo shopcart.cli.
  • [tool.pytest.ini_options] é a configuração específica do pytest. Cada ferramenta tem seu próprio namespace [tool.<nome>].

Com src layout, setuptools encontra automaticamente o pacote em src/, então você não precisa declarar nada extra.

4.3. Código do pacote

# src/shopcart/order.py
from dataclasses import dataclass, field
from decimal import Decimal


@dataclass(frozen=True)
class OrderLine:
    product: str
    unit_price: Decimal
    quantity: int


@dataclass
class Order:
    customer: str
    lines: list[OrderLine] = field(default_factory=list)

    def add(self, product: str, unit_price: str, quantity: int = 1) -> None:
        if quantity <= 0:
            raise ValueError("quantidade deve ser maior que 0")
        self.lines.append(OrderLine(product, Decimal(unit_price), quantity))

    def total(self) -> Decimal:
        return sum((l.unit_price * l.quantity for l in self.lines), Decimal("0"))

Order mantém uma lista de OrderLine. Os preços usam Decimal para evitar erros de ponto flutuante, então unit_price é passado como uma string. O método total() soma o total de cada linha, começando com Decimal("0").

# src/shopcart/cli.py
from shopcart.order import Order


def main() -> None:
    order = Order(customer="Nguyễn Văn An")
    order.add("Café com leite", "29000", 2)
    order.add("Pão francês", "25000")
    print(f"Cliente: {order.customer}")
    print(f"Total: {order.total():,} VND")

O arquivo src/shopcart/__init__.py pode estar vazio.

4.4. Instalando no modo editável

python -m pip install -e ".[dev]"
shopcart
Cliente: Nguyễn Văn An
Total: 83,000 VND

A flag -e (editável) vincula o pacote ao código-fonte em vez de copiá-lo para site-packages. Dessa forma, quando você edita order.py, as mudanças têm efeito imediato sem precisar reinstalar. A parte ".[dev]" significa instalar o projeto no diretório atual com o grupo dev, então pytest e ruff também são instalados. O comando shopcart está disponível no terminal graças à declaração em [project.scripts].

Nota: Alguns shells como zsh interpretam [...] como um glob pattern. Por isso sempre coloque ".[dev]" entre aspas.

5. Escrevendo e executando testes

# tests/test_order.py
from decimal import Decimal

import pytest

from shopcart.order import Order


def test_total_sums_all_lines():
    order = Order(customer="Trần Thị Bình")
    order.add("Chá de pêssego", "35000", 2)
    order.add("Flan", "15000", 3)

    assert order.total() == Decimal("115000")


def test_empty_order_has_zero_total():
    assert Order(customer="Lê Minh").total() == Decimal("0")


def test_add_rejects_non_positive_quantity():
    order = Order(customer="Phạm Hà")
    with pytest.raises(ValueError, match="quantidade"):
        order.add("Água mineral", "10000", 0)
python -m pytest -q
...                                                     [100%]
3 passed in 0.02s

Os testes importam shopcart.order como um pacote instalado. Essa é a razão pela qual você precisa do passo pip install -e acima. Se não estiver instalado, o src layout relatará ModuleNotFoundError, e esse erro é intencional: força você a testar exatamente o que os usuários instalarão. O terceiro teste usa pytest.raises para verificar que uma quantidade inválida é rejeitada com uma mensagem contendo a palavra “quantidade”.

6. Travando versões: pyproject.toml ou requirements.txt?

Esses dois arquivos não se excluem mutuamente. Eles servem a dois propósitos diferentes:

pyproject.toml Lock file (requirements.txt travado)
Objetivo Descrever o que o projeto precisa Reproduzir exatamente um ambiente
Versões Intervalos (>=2.31,<3) Fixas (==2.32.3)
Para Bibliotecas e aplicações Aplicações em produção (servidor, Docker)

Para uma biblioteca que será publicada no PyPI, você só precisa de pyproject.toml. Se você travar as versões, os usuários da sua biblioteca enfrentarão conflitos.

Para uma aplicação rodando em um servidor, você deve ter um lock file adicional para que cada deploy instale o mesmo conjunto de versões:

python -m pip install -e .
python -m pip freeze --exclude-editable > requirements.lock

Ao fazer deploy, instale do lock file primeiro, depois instale o projeto, para evitar que pip atualize as versões das dependências:

python -m pip install -r requirements.lock
python -m pip install --no-deps .

Se quiser um processo de lock mais enxuto, você pode usar pip-tools (comando pip-compile pyproject.toml) ou uv. Ambos leem diretamente pyproject.toml e geram um lock file com hashes.

7. Erros comuns

  • Instalar pacotes com sudo pip install. Esse comando sobrescreve pacotes do sistema operacional e pode danificar ferramentas do sistema. Muitas distribuições Linux modernas bloqueiam completamente isso com o erro externally-managed-environment. A solução é sempre usar um venv.
  • Esquecer de ativar o venv antes de instalar ou executar. Você pode verificar usando which python como na seção 2.2, ou chamar diretamente .venv/bin/python -m pip ....
  • Criar venv com uma versão do Python e executar com outra. O venv está firmemente vinculado ao interpretador que o criou. Quando você atualiza o Python (por exemplo, de 3.11 para 3.12), delete .venv e recrie.
  • Mover ou renomear o diretório do projeto. Os scripts em .venv/bin contêm caminhos absolutos, então quebram quando movidos. Você precisa recriar o venv.
  • Misturar setup.py antigo com pyproject.toml e declarar dependências em ambos. Escolha uma única fonte de verdade. Para projetos novos, esse é a seção [project].
  • Colocar ferramentas de desenvolvimento em dependencies. pytest ou ruff devem estar em optional-dependencies (ou [dependency-groups] se sua ferramenta suportar), para que usuários finais não precisem instalá-las.

Nota: Não use venv quando precisa apenas executar uma ferramenta CLI independente como black ou httpie globalmente. Nesse caso, pipx é mais apropriado: cria automaticamente um venv separado para cada ferramenta e adiciona o comando ao PATH.

8. Conclusão

Este artigo cobriu as três ferramentas fundamentais de um projeto Python:

  • python -m venv .venv isola as dependências para cada projeto.
  • python -m pip instala pacotes no interpretador correto em uso.
  • pyproject.toml declara metadados, dependências, grupos opcionais, comandos CLI e configuração de ferramentas em um único arquivo.

Combinados com src layout, pip install -e ".[dev]" e pytest, você tem um projeto limpo, fácil de testar e fácil de compartilhar. Para aplicações que precisam de deployments estáveis, adicione um lock file junto a pyproject.toml para garantir que todo ambiente instale exatamente o mesmo conjunto de versões.

pythonpipvenvpyprojectpackaging