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 .venv a 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 pip actualiza pip mismo. Las versiones antiguas de pip a veces no leen pyproject.toml correctamente.
  • "requests>=2.31,<3" es un especificador de versión. Debes ponerlo entre comillas para que el shell no interprete < como una redirección.
  • list enumera los paquetes instalados. show imprime 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 con hatchling o flit_core sin modificar el resto.
  • [project] contiene metadatos estándar. requires-python evita que instalen el proyecto en versiones de Python no soportadas.
  • dependencies son 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 grupo dev contiene herramientas de desarrollo que los usuarios finales no necesitan.
  • [project.scripts] crea el comando shopcart en la terminal. Este comando llama a la función main en el módulo shopcart.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 error externally-managed-environment. La solución es usar siempre venv.
  • Olvidar activar venv antes de instalar o ejecutar. Puedes verificar con which python como 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 .venv y recréalo.
  • Mover o renombrar el directorio del proyecto. Los scripts en .venv/bin contienen rutas absolutas, así que se rompen después de mover. Necesitas recrear el venv.
  • Mezclar setup.py antiguo con pyproject.toml y declarar dependencias en ambos lugares. Elige una única fuente de verdad. Para proyectos nuevos, esa es la tabla [project].
  • Poner herramientas dev en dependencies. pytest o ruff deberían estar en optional-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 black o httpie en toda la máquina. En este caso, pipx es más apropiado: crea automáticamente un venv independiente para cada herramienta y añade el comando al PATH.

8. Conclusión

Este artículo ha cubierto la triada de herramientas fundamentales de un proyecto Python:

  • python -m venv .venv aísla las dependencias para cada proyecto.
  • python -m pip instala paquetes en el intérprete correcto que estás usando.
  • pyproject.toml declara 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.

pythonpipvenvpyprojectpackaging