Entorno virtual, pip y pyproject.toml en Python
1. Descripción general
Cuando trabajes en múltiples proyectos Python en una misma máquina, pronto encontrarás conflictos de versiones. El proyecto A necesita requests 2.28, mientras que el proyecto B necesita requests 2.32. Este artículo te guía a través de tres herramientas para resolver ese problema. venv aísla el entorno para cada proyecto. pip instala y gestiona paquetes. pyproject.toml declara el proyecto según los estándares modernos. Después de leerlo, podrás crear un proyecto Python con estructura estándar, instalable en modo editable y con tests ejecutables con pytest.
2. Entorno virtual
Un entorno virtual (venv) es un directorio que contiene tres cosas: su propio intérprete de Python (generalmente un symlink), su propio directorio site-packages y scripts de activación. Los paquetes que instales en venv solo se ubicarán en ese directorio, sin afectar el Python del sistema.
2.1. Crear y activar un venv
El módulo venv viene incluido en la librería estándar desde Python 3.3, así que no necesitas instalar nada adicional:
mkdir shopcart && cd shopcart
python3 -m venv .venv
# Linux / macOS
source .venv/bin/activate
# Windows (PowerShell)
.venv\Scripts\Activate.ps1
El comando python3 -m venv .venv crea el directorio .venv en tu proyecto. Nombrar .venv es una convención común, y la mayoría de IDEs lo reconocerán automáticamente. Después de activarlo, el prompt del shell tendrá el prefijo (.venv). Los comandos python y pip ahora apuntan al venv.
Para salir del venv, ejecuta:
deactivate
2.2. Verifica qué intérprete estás usando
Este es un paso que muchos saltan, y es la fuente de muchos errores como “ya lo instalé pero sigue apareciendo 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 es el directorio raíz del entorno que se está ejecutando. sys.base_prefix es el directorio de la instalación base de Python. Si son diferentes, significa que estás dentro de un venv.
Nota: No commits el directorio
.venva Git. Añade.venv/a.gitignore. Los venv dependen de rutas absolutas y del sistema operativo, así que copiarlos a otra máquina generalmente falla. La forma correcta es hacer commit de los archivos de declaración de dependencias y recrear el venv en la nueva máquina.
3. Gestionar paquetes con pip
pip es el instalador de paquetes predeterminado, que obtiene paquetes de 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 hace lo siguiente:
install --upgrade pipactualiza pip mismo. Las versiones antiguas de pip a veces no leenpyproject.tomlcorrectamente."requests>=2.31,<3"es un especificador de versión. Debes ponerlo entre comillas para que el shell no interprete<como una redirección.listenumera los paquetes instalados.showimprime información detallada, incluyendo versión, ubicación de instalación y paquetes de los que depende.
3.2. Por qué debes usar python -m pip en lugar de pip
El comando pip puro puede apuntar a un Python diferente del que estás usando. Esto sucede frecuentemente cuando la máquina tiene múltiples versiones de Python, o cuando olvidas activar el venv. El comando python -m pip siempre ejecuta pip del intérprete correcto, por lo que los paquetes se instalan definitivamente en el entorno que deseas.
3.3. requirements.txt y pip freeze
La forma tradicional de compartir una lista de paquetes es 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 los paquetes en el venv con su versión exacta, incluyendo dependencias indirectas como urllib3. Este enfoque es apropiado para reproducir exactamente un entorno de ejecución. Sin embargo, no indica qué paquetes necesitas realmente y cuáles solo se incluyen como dependencias. Tampoco describe tu proyecto: nombre, versión, requisitos de Python. Eso es trabajo de pyproject.toml.
4. Declarar un proyecto con pyproject.toml
pyproject.toml es el archivo de configuración estándar para un proyecto Python. Se define en los estándares PEP 518, PEP 621 y PEPs relacionados. Este archivo reemplaza setup.py y setup.cfg, y consolida la configuración de múltiples herramientas como pytest o ruff en un único lugar.
4.1. Estructura del proyecto
Usamos src layout, es decir, colocamos el código en src/. Esta estructura asegura que los tests siempre se ejecuten contra el paquete instalado, en lugar de importar accidentalmente desde el directorio de trabajo:
shopcart/
├── pyproject.toml
├── src/
│ └── shopcart/
│ ├── __init__.py
│ ├── order.py
│ └── cli.py
└── tests/
└── test_order.py
4.2. Archivo pyproject.toml completo
[build-system]
requires = ["setuptools>=69"]
build-backend = "setuptools.build_meta"
[project]
name = "shopcart"
version = "0.1.0"
description = "Calcular el total de un pedido simple"
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"]
El significado de cada sección:
[build-system]le dice a pip qué herramientas necesita para compilar el proyecto. Aquí es setuptools. Puedes reemplazarlo conhatchlingoflit_coresin modificar el resto.[project]contiene metadatos estándar.requires-pythonevita que instalen el proyecto en versiones de Python no soportadas.dependenciesson los paquetes obligatorios para ejecutar el proyecto. Aquí debes declarar rangos de versión (>=2.31,<3) en lugar de fijar exactamente (==2.32.3).[project.optional-dependencies]agrupa paquetes opcionales. El grupodevcontiene herramientas de desarrollo que los usuarios finales no necesitan.[project.scripts]crea el comandoshopcarten la terminal. Este comando llama a la funciónmainen el móduloshopcart.cli.[tool.pytest.ini_options]es la configuración específica de pytest. Cada herramienta tiene su propio espacio de nombres[tool.<nombre>].
Con src layout, setuptools encuentra automáticamente los paquetes en src/, así que no necesitas declaraciones adicionales.
4.3. Código del paquete
# 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("quantity debe ser mayor 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 mantiene una lista de OrderLine. Los precios usan Decimal para evitar errores de punto flotante, así que unit_price se pasa como cadena. El método total() suma el monto de cada línea, con un valor inicial de Decimal("0").
# src/shopcart/cli.py
from shopcart.order import Order
def main() -> None:
order = Order(customer="Nguyễn Văn An")
order.add("Cà phê sữa", "29000", 2)
order.add("Bánh mì", "25000")
print(f"Cliente: {order.customer}")
print(f"Total: {order.total():,} VND")
El archivo src/shopcart/__init__.py puede estar vacío.
4.4. Instalar en modo editable
python -m pip install -e ".[dev]"
shopcart
Cliente: Nguyễn Văn An
Total: 83,000 VND
La bandera -e (editable) vincula el paquete al código fuente en lugar de copiarlo a site-packages. Así, cuando edites order.py, los cambios entran en vigor inmediatamente sin necesidad de reinstalar. La parte ".[dev]" significa instalar el proyecto en el directorio actual junto con el grupo dev, así que también se instalan pytest y ruff. El comando shopcart está disponible en la terminal gracias a la declaración [project.scripts].
Nota: Algunos shells como zsh interpretan
[...]como un patrón glob. Por lo tanto, siempre debes poner".[dev]"entre comillas.
5. Escribir y ejecutar tests
# 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("Trà đào", "35000", 2)
order.add("Bánh 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="quantity"):
order.add("Nước suối", "10000", 0)
python -m pytest -q
... [100%]
3 passed in 0.02s
Los tests importan shopcart.order como un paquete instalado. Esta es la razón por la que necesitas el paso pip install -e anterior. Si no lo instalas, src layout reportará ModuleNotFoundError, y este error es intencional: te obliga a probar exactamente lo que los usuarios instalarán. El tercer test usa pytest.raises para verificar que una cantidad inválida sea rechazada con un mensaje que contiene la palabra “quantity”.
6. Bloquear versiones
Estos dos archivos no se excluyen mutuamente. Sirven para dos propósitos diferentes:
pyproject.toml |
Archivo lock (requirements.txt fijado) |
|
|---|---|---|
| Propósito | Describe qué necesita el proyecto | Reproduce exactamente un entorno |
| Versión | Rango (>=2.31,<3) |
Fijada (==2.32.3) |
| Usar para | Librerías y aplicaciones | Aplicaciones deployadas (servidor, Docker) |
Para una librería que publicarás en PyPI, solo necesitas pyproject.toml. Si fijas las versiones exactamente, los usuarios de tu librería enfrentarán conflictos.
Para una aplicación que se ejecuta en un servidor, debes tener un archivo lock adicional para asegurar que cada deploy instale exactamente el mismo conjunto de versiones:
python -m pip install -e .
python -m pip freeze --exclude-editable > requirements.lock
Cuando hagas deploy, instala primero desde el archivo lock, luego el proyecto, para que pip no suba automáticamente las versiones de las dependencias:
python -m pip install -r requirements.lock
python -m pip install --no-deps .
Si quieres un proceso de bloqueo más limpio, puedes usar pip-tools (comando pip-compile pyproject.toml) o uv. Ambos leen directamente pyproject.toml y generan un archivo lock con hashes.
7. Errores comunes
- Instalar paquetes con
sudo pip install. Este comando sobrescribe paquetes del sistema operativo y puede romper herramientas del sistema. Muchas distribuciones Linux modernas lo bloquean directamente con el errorexternally-managed-environment. La solución es usar siempre venv. - Olvidar activar venv antes de instalar o ejecutar. Puedes verificar con
which pythoncomo en la sección 2.2, o llamar directamente a.venv/bin/python -m pip .... - Crear venv con un Python y ejecutar con otro. Un venv está estrechamente vinculado al intérprete que lo creó. Cuando actualices Python (por ejemplo, de 3.11 a 3.12), borra
.venvy recréalo. - Mover o renombrar el directorio del proyecto. Los scripts en
.venv/bincontienen rutas absolutas, así que se rompen después de mover. Necesitas recrear el venv. - Mezclar
setup.pyantiguo conpyproject.tomly declarar dependencias en ambos lugares. Elige una única fuente de verdad. Para proyectos nuevos, esa es la tabla[project]. - Poner herramientas dev en
dependencies.pytestoruffdeberían estar enoptional-dependencies(o[dependency-groups]si tu herramienta lo soporta), para que los usuarios finales no tengan que instalarlas.
Nota: No uses venv si solo necesitas ejecutar una herramienta CLI independiente como
blackohttpieen toda la máquina. En este caso,pipxes más apropiado: crea automáticamente un venv independiente para cada herramienta y añade el comando alPATH.
8. Conclusión
Este artículo ha cubierto la triada de herramientas fundamentales de un proyecto Python:
python -m venv .venvaísla las dependencias para cada proyecto.python -m pipinstala paquetes en el intérprete correcto que estás usando.pyproject.tomldeclara metadatos, dependencias, grupos opcionales, comandos CLI y configuración de herramientas en un único archivo.
Combinando src layout, pip install -e ".[dev]" y pytest, tienes un proyecto limpio, fácil de probar y fácil de compartir. Para aplicaciones que necesitan deployments estables, añade un archivo lock junto a pyproject.toml para asegurar que todos los entornos instalen exactamente el mismo conjunto de versiones.