Módulo 2 15 min de lectura

02 - Gestión de Entornos y Dependencias

De npm a uv: entornos virtuales, pyproject.toml, lockfiles deterministas y el tooling moderno de Python.

#uv #pip #virtualenv #pyproject #dependencies

1. El Problema que Node.js Resolvió (y Python Tardó en Resolver)

En Node.js, el aislamiento de dependencias es automático: node_modules vive en tu proyecto y package-lock.json garantiza builds reproducibles. En Python, históricamente teníamos:

En 2024-2025, esto cambió radicalmente con uv.


2. uv: El npm de Python (Escrito en Rust)

uv es un gestor de paquetes y entornos virtuales creado por Astral (los mismos de Ruff). Es 10-100x más rápido que pip y unifica:

Instalación

# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# O con pipx (si ya tienes Python)
pipx install uv

Workflow Básico

JavaScript/TypeScript
# Node.js
npm init -y
npm install fastapi uvicorn
npm install -D pytest ruff

# Ejecutar
npx uvicorn main:app
Python
# Python con uv
uv init
uv add fastapi uvicorn
uv add --dev pytest ruff

# Ejecutar
uv run uvicorn main:app

Estructura de Proyecto Generada

my-project/
├── .venv/              # Entorno virtual (equivalente a node_modules)
├── .python-version     # Versión de Python pinneada
├── pyproject.toml      # Equivalente a package.json
├── uv.lock             # Lockfile determinista (como package-lock.json)
└── src/
    └── my_project/
        └── __init__.py

3. pyproject.toml: El package.json de Python

Este archivo centraliza toda la configuración del proyecto (PEP 621):

[project]
name = "mi-api"
version = "1.0.0"
description = "API Enterprise con FastAPI"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "fastapi>=0.109.0",
    "uvicorn[standard]>=0.27.0",
    "pydantic>=2.6.0",
    "sqlalchemy>=2.0.25",
    "asyncpg>=0.29.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0.0",
    "pytest-asyncio>=0.23.0",
    "ruff>=0.2.0",
    "mypy>=1.8.0",
]

[project.scripts]
# Equivalente a "scripts" en package.json
start = "uvicorn src.main:app --reload"
test = "pytest"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

# Configuración de Ruff (linter + formatter)
[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "A", "C4", "PT"]

# Configuración de Pytest
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

# Configuración de Mypy
[tool.mypy]
python_version = "3.12"
strict = true

Comparativa de Campos

package.jsonpyproject.tomlNotas
name[project] nameIdéntico
version[project] versionIdéntico
dependencies[project] dependenciesLista en vez de objeto
devDependencies[project.optional-dependencies] devGrupo “dev”
scripts[project.scripts]Entry points ejecutables
mainN/APython usa __init__.py
type: "module"N/APython siempre es “module”

4. Entornos Virtuales: El Aislamiento Real

A diferencia de node_modules, los entornos virtuales de Python son directorios con binarios y symlinks:

.venv/
├── bin/
   ├── python -> /usr/local/bin/python3.12  # Symlink al intérprete
   ├── pip                                    # pip aislado
   ├── uvicorn                                # Binarios instalados
   └── activate                               # Script de activación
├── lib/
   └── python3.12/
       └── site-packages/                     # Dependencias instaladas
└── pyvenv.cfg                                 # Configuración del venv

Activación (Opcional con uv)

# Método tradicional (sin uv)
source .venv/bin/activate
python -m uvicorn main:app

# Con uv (no necesitas activar)
uv run uvicorn main:app

uv run automáticamente:

  1. Detecta si existe .venv
  2. Lo crea si no existe
  3. Sincroniza dependencias con uv.lock
  4. Ejecuta el comando en el entorno correcto

5. Lockfiles: Reproducibilidad Garantizada

El Problema de requirements.txt

# requirements.txt tradicional - ¡PELIGROSO!
fastapi>=0.100.0  # ¿Qué versión exacta?
pydantic          # ¿2.0? ¿2.6? ¿2.9?

Cada pip install puede instalar versiones diferentes, causando “works on my machine”.

La Solución: uv.lock

uv.lock es un lockfile completo que incluye:

# uv.lock (generado automáticamente)
[[package]]
name = "fastapi"
version = "0.109.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "pydantic" },
    { name = "starlette" },
]

[[package]]
name = "pydantic"
version = "2.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "...", hash = "sha256:..." }

Workflow de Lockfile

# Desarrollo: añadir dependencia y actualizar lock
uv add httpx
# → Modifica pyproject.toml
# → Actualiza uv.lock
# → Instala en .venv

# CI/CD: instalación reproducible
uv sync --frozen
# → Lee uv.lock exactamente
# → Falla si el lock está desactualizado

# Actualizar dependencias
uv lock --upgrade-package fastapi
# → Solo actualiza fastapi y sus dependencias

6. Gestión de Versiones de Python

uv también gestiona las versiones de Python (como nvm para Node):

# Listar versiones disponibles
uv python list

# Instalar una versión específica
uv python install 3.12

# Pinear versión para el proyecto
uv python pin 3.12
# → Crea .python-version con "3.12"

# uv usará automáticamente la versión correcta
uv run python --version  # Python 3.12.x

.python-version

3.12

Este archivo (como .nvmrc) asegura que todos los developers usen la misma versión.


7. Workspaces (Monorepos)

Para proyectos enterprise con múltiples packages:

# pyproject.toml (raíz)
[tool.uv.workspace]
members = ["packages/*"]

[tool.uv.sources]
# Dependencias locales
shared-lib = { workspace = true }
monorepo/
├── pyproject.toml          # Workspace root
├── uv.lock                  # Lock compartido
├── packages/
│   ├── api/
│   │   └── pyproject.toml   # depends on shared-lib
│   ├── worker/
│   │   └── pyproject.toml   # depends on shared-lib
│   └── shared-lib/
│       └── pyproject.toml

8. Migración desde pip/poetry

Desde requirements.txt

# Crear pyproject.toml desde requirements
uv init
uv add $(cat requirements.txt)

Desde Poetry

# uv puede leer poetry.lock
uv sync  # Genera uv.lock desde poetry.lock

Desde Pipenv

# Exportar primero
pipenv requirements > requirements.txt
uv init
uv add -r requirements.txt

9. Integración con Docker

El workflow de Docker cambia significativamente con uv:

# Dockerfile optimizado con uv
FROM python:3.12-slim AS builder

# Instalar uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

# Copiar solo archivos de dependencias (layer caching)
WORKDIR /app
COPY pyproject.toml uv.lock ./

# Instalar dependencias (sin el código fuente)
RUN uv sync --frozen --no-dev --no-install-project

# Copiar código fuente
COPY src ./src

# Instalar el proyecto
RUN uv sync --frozen --no-dev

# Runtime stage
FROM python:3.12-slim
WORKDIR /app

# Copiar el venv completo
COPY --from=builder /app/.venv ./.venv
ENV PATH="/app/.venv/bin:$PATH"

CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

Optimizaciones aplicadas:

  1. Layer caching: Dependencias se reinstalan solo si pyproject.toml o uv.lock cambian
  2. Multi-stage: Builder tiene uv, runtime solo tiene Python
  3. —frozen: Falla si el lockfile está desactualizado (seguridad en CI)

10. Tabla Comparativa: Ecosistema de Gestión

HerramientaVelocidadLockfilePython Version MgmtRecomendación
uv⚡ 10-100x pipuv.lock✅ IntegradoUsar siempre
pip🐌 LentoLegacy
poetry🐢 Moderadopoetry.lockMigrar a uv
pipenv🐢 LentoPipfile.lockObsoleto
conda🐢 LentoSolo para Data Science

11. Scripts y Comandos Comunes

# Inicializar proyecto
uv init my-project
cd my-project

# Añadir dependencias
uv add fastapi uvicorn[standard]
uv add --dev pytest ruff mypy

# Ejecutar comandos
uv run pytest
uv run ruff check .
uv run uvicorn main:app --reload

# Actualizar dependencias
uv lock --upgrade              # Todas
uv lock --upgrade-package X    # Solo X

# Limpiar y reinstalar
rm -rf .venv
uv sync

# Exportar para sistemas legacy
uv export > requirements.txt

Conclusión

El tooling de Python ha madurado dramáticamente. Con uv:

  1. Velocidad: Instalaciones 10-100x más rápidas que pip
  2. Reproducibilidad: Lockfiles deterministas como en Node.js
  3. Simplicidad: Una herramienta para todo (venv, deps, Python versions)
  4. Compatibilidad: Lee requirements.txt, poetry.lock, etc.

Tu workflow diario será:

uv add <package>      # Añadir dependencia
uv run <command>      # Ejecutar en el venv
uv sync --frozen      # CI/CD reproducible

En el siguiente capítulo, profundizaremos en el sistema de tipos de Python: Type Hints, Pydantic V2, Generics y Protocols.