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
.venvno 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 pipatualiza o próprio pip. Versões antigas do pip às vezes não leempyproject.tomlcorretamente."requests>=2.31,<3"é um especificador de versão. Você precisa colocá-lo entre aspas para o shell não interpretar<como redirecionamento.listlista os pacotes instalados.showimprime 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 porhatchlingouflit_coresem alterar o resto.[project]contém metadados padrão.requires-pythonimpede que outras pessoas instalem o projeto em versões não suportadas do Python.dependenciessã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 grupodevcontém ferramentas de desenvolvimento que usuários finais não precisam.[project.scripts]cria o comandoshopcartno terminal. Esse comando chama a funçãomaindo móduloshopcart.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 erroexternally-managed-environment. A solução é sempre usar um venv. - Esquecer de ativar o venv antes de instalar ou executar. Você pode verificar usando
which pythoncomo 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
.venve recrie. - Mover ou renomear o diretório do projeto. Os scripts em
.venv/bincontêm caminhos absolutos, então quebram quando movidos. Você precisa recriar o venv. - Misturar
setup.pyantigo compyproject.tomle 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.pytestouruffdevem estar emoptional-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
blackouhttpieglobalmente. Nesse caso,pipxé mais apropriado: cria automaticamente um venv separado para cada ferramenta e adiciona o comando aoPATH.
8. Conclusão
Este artigo cobriu as três ferramentas fundamentais de um projeto Python:
python -m venv .venvisola as dependências para cada projeto.python -m pipinstala pacotes no interpretador correto em uso.pyproject.tomldeclara 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.