Packaging Python Applications
Empaqueta, distribuye y comparte tu código con el mundo
Crear código Python es solo el primer paso. Para que otros puedan usar tu trabajo, necesitas empaquetarlo y distribuirlo de manera profesional. Este capítulo explora el ecosistema moderno de empaquetado en Python, desde la estructura de un proyecto hasta su publicación en PyPI.
Aprenderás a usar pyproject.toml, los diferentes build backends, cómo crear wheels y source distributions, y cómo publicar tu paquete para que millones de desarrolladores puedan instalarlo con un simple pip install.
¿Por qué Empaquetar?
El empaquetado de aplicaciones Python transforma tu código de un conjunto de archivos locales en un artefacto distribuible que otros pueden instalar y usar fácilmente. Es el puente entre desarrollar y compartir.
Beneficios del Empaquetado
Estructura del Proyecto
Un proyecto Python bien estructurado sigue convenciones que facilitan el empaquetado. La estructura src layout es la recomendada actualmente por la comunidad.
Layout Recomendado
Archivos Esenciales
| Archivo | Propósito |
|---|---|
| pyproject.toml | Configuración del proyecto y metadatos |
| README.md | Documentación y descripción del proyecto |
| LICENSE | Términos de uso y distribución |
| __init__.py | Marca el directorio como paquete Python |
src layout previene importaciones accidentales del código fuente durante el desarrollo, asegurando que siempre uses la versión instalada del paquete.
pyproject.toml
El archivo pyproject.toml es el estándar moderno para configurar proyectos Python. Reemplaza a los antiguos setup.py y setup.cfg con un formato más limpio y declarativo basado en TOML.
Estructura Básica
# Sistema de build [build-system] requires = ["hatchling"] build-backend = "hatchling.build" # Metadatos del proyecto [project] name = "mi-paquete" version = "1.0.0" description = "Un paquete Python increíble" readme = "README.md" license = "MIT" requires-python = ">=3.9" authors = [ { name = "Tu Nombre", email = "tu@email.com" } ] keywords = ["python", "packaging", "ejemplo"] classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] # Dependencias dependencies = [ "requests>=2.28.0", "click>=8.0.0", ] # Dependencias opcionales [project.optional-dependencies] dev = [ "pytest>=7.0.0", "black>=23.0.0", "mypy>=1.0.0", ] docs = [ "sphinx>=6.0.0", "sphinx-rtd-theme", ] # URLs del proyecto [project.urls] Homepage = "https://github.com/usuario/mi-paquete" Documentation = "https://mi-paquete.readthedocs.io" Repository = "https://github.com/usuario/mi-paquete" Issues = "https://github.com/usuario/mi-paquete/issues"
Campos Obligatorios vs Opcionales
mi_usuario_paquete.
Build Backends
El build backend es la herramienta que convierte tu código fuente en paquetes distribuibles. Python soporta múltiples backends, cada uno con sus propias características y filosofía.
Comparativa de Backends
| Backend | Características | Caso de Uso |
|---|---|---|
| Hatchling | Rápido, moderno, extensible | Proyectos nuevos, paquetes puros Python |
| Setuptools | Maduro, ampliamente soportado | Proyectos existentes, extensiones C |
| Flit | Minimalista, simple | Paquetes simples sin compilación |
| PDM | Gestor de dependencias integrado | Proyectos con gestión avanzada |
| Poetry | Todo en uno, popular | Proyectos que quieren ecosistema completo |
Configuración por Backend
[build-system] requires = ["hatchling >= 1.26"] build-backend = "hatchling.build"
[build-system] requires = ["setuptools >= 77.0", "wheel"] build-backend = "setuptools.build_meta"
[build-system] requires = ["flit_core >= 3.12.0, <4"] build-backend = "flit_core.buildapi"
setuptools por defecto. Sin embargo, es mejor ser explícito para evitar sorpresas.
Entry Points y Scripts
Los entry points permiten que tu paquete exponga funcionalidades que otros pueden descubrir y usar, incluyendo comandos de terminal que se crean automáticamente al instalar el paquete.
Console Scripts
Define comandos CLI que estarán disponibles después de la instalación:
[project.scripts] mi-comando = "mi_paquete.cli:main" otro-comando = "mi_paquete.tools:run"
import click @click.command() @click.option("--name", default="World", help="Nombre a saludar") def main(name: str) -> None: """CLI principal del paquete.""" click.echo(f"¡Hola, {name}!") if __name__ == "__main__": main()
¡Hola, Python!
GUI Scripts
Para aplicaciones gráficas (sin ventana de consola en Windows):
[project.gui-scripts] mi-app-gui = "mi_paquete.gui:launch"
Plugins con Entry Points
Los entry points también permiten crear sistemas de plugins:
[project.entry-points."mi_app.plugins"] json_exporter = "mi_paquete.exporters:JSONExporter" csv_exporter = "mi_paquete.exporters:CSVExporter"
from importlib.metadata import entry_points # Descubrir todos los plugins registrados plugins = entry_points(group="mi_app.plugins") for plugin in plugins: print(f"Plugin: {plugin.name}") exporter_class = plugin.load() # Carga la clase exporter = exporter_class() # Instancia
Formatos de Distribución
Python soporta dos formatos principales de distribución: source distributions (sdist) y wheels. Cada uno tiene propósitos diferentes y es importante entender cuándo usar cada uno.
Source Distribution (sdist)
Wheel
Anatomía de un Wheel
# Formato: {nombre}-{version}-{python}-{abi}-{plataforma}.whl mi_paquete-1.0.0-py3-none-any.whl # │ │ │ │ │ # │ │ │ │ └── any: cualquier plataforma # │ │ │ └── none: sin ABI específico # │ │ └── py3: Python 3.x # │ └── versión # └── nombre del paquete numpy-1.24.0-cp311-cp311-manylinux_x86_64.whl # Wheel con extensiones C para CPython 3.11 en Linux x86_64
py3-none-any indica un wheel "universal" de Python puro que funciona en cualquier plataforma. Los paquetes con extensiones C necesitan wheels específicos por plataforma.
Construir el Paquete
El proceso de build convierte tu código fuente en artefactos distribuibles. La herramienta estándar es build, que funciona con cualquier backend configurado en pyproject.toml.
Instalación de Herramientas
# Instalar build y twine $ pip install build twine # Verificar instalación $ python -m build --version $ twine --version
Proceso de Build
# Desde el directorio raíz del proyecto $ cd mi_proyecto/ # Construir sdist y wheel $ python -m build # Solo sdist $ python -m build --sdist # Solo wheel $ python -m build --wheel
* Installing packages in isolated environment...
* Building sdist...
* Building wheel from sdist...
Successfully built mi_paquete-1.0.0.tar.gz and mi_paquete-1.0.0-py3-none-any.whl
Estructura del dist/
Verificar el Contenido
# Ver contenido del wheel $ unzip -l dist/mi_paquete-1.0.0-py3-none-any.whl # Ver contenido del sdist $ tar -tvf dist/mi_paquete-1.0.0.tar.gz # Verificar que el paquete es válido para PyPI $ twine check dist/*
Checking dist/mi_paquete-1.0.0.tar.gz: PASSED
twine check antes de subir a PyPI. Detecta problemas como descripciones mal formateadas que causarían errores al publicar.
Instalación en Modo Editable
Durante el desarrollo, necesitas probar cambios sin reinstalar constantemente. El modo editable (o desarrollo) vincula tu código fuente directamente, reflejando cambios inmediatamente.
Instalación Editable
# Instalación editable básica $ pip install -e . # Con dependencias de desarrollo $ pip install -e ".[dev]" # Con múltiples grupos de extras $ pip install -e ".[dev,docs,test]"
Verificar la Instalación
# Ver información del paquete instalado $ pip show mi-paquete # Listar archivos instalados $ pip show -f mi-paquete # Verificar que funciona $ python -c "import mi_paquete; print(mi_paquete.__version__)"
Version: 1.0.0
Location: /home/user/mi_proyecto/src
Editable project location: /home/user/mi_proyecto
pyproject.toml (como nuevos entry points) requieren reinstalar: pip install -e .
Publicar en PyPI
PyPI (Python Package Index) es el repositorio oficial donde se publican paquetes Python. Una vez publicado, cualquiera puede instalar tu paquete con pip install.
Proceso de Publicación
Configurar Credenciales
[pypi] username = __token__ password = pypi-AgEIcHlwaS5vcmc... # Tu API token [testpypi] username = __token__ password = pypi-AgENdGVzdC5weXBp... # Token de TestPyPI
Probar en TestPyPI
# Primero, probar en TestPyPI (repositorio de prueba) $ twine upload --repository testpypi dist/* # Verificar instalación desde TestPyPI $ pip install --index-url https://test.pypi.org/simple/ mi-paquete
Publicar en PyPI Oficial
# Subir a PyPI oficial $ twine upload dist/* # Verificar que está disponible $ pip install mi-paquete
Uploading mi_paquete-1.0.0-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━ 15.2/15.2 kB
Uploading mi_paquete-1.0.0.tar.gz
100% ━━━━━━━━━━━━━━━━ 12.8/12.8 kB
View at: https://pypi.org/project/mi-paquete/1.0.0/
Versionado Semántico
El Semantic Versioning (SemVer) es un estándar para nombrar versiones que comunica la naturaleza de los cambios a los usuarios de tu paquete.
Significado de cada Número
| Componente | Cuándo Incrementar | Ejemplo |
|---|---|---|
| MAJOR | Cambios incompatibles (breaking changes) | 1.0.0 → 2.0.0 |
| MINOR | Nueva funcionalidad compatible | 1.0.0 → 1.1.0 |
| PATCH | Corrección de bugs | 1.0.0 → 1.0.1 |
Versiones Pre-release
"0.1.0" # Desarrollo inicial "1.0.0a1" # Alpha 1 "1.0.0b1" # Beta 1 "1.0.0rc1" # Release Candidate 1 "1.0.0" # Versión estable "1.0.1" # Patch con bugfix "1.1.0" # Nueva funcionalidad "2.0.0" # Breaking change
Versión Dinámica desde Git
[project] name = "mi-paquete" dynamic = ["version"] # Versión calculada dinámicamente [tool.hatch.version] source = "vcs" # Obtener de Git tags [tool.hatch.build.hooks.vcs] version-file = "src/mi_paquete/_version.py"
git tag v1.0.0) y el build system extraerá la versión automáticamente. Elimina la sincronización manual.
Incluyendo Archivos Adicionales
Además del código Python, tu paquete puede necesitar incluir archivos de datos, configuración, plantillas u otros recursos. La forma de incluirlos depende del build backend.
Package Data con Hatchling
[tool.hatch.build.targets.wheel] packages = ["src/mi_paquete"] [tool.hatch.build.targets.sdist] include = [ "/src", "/tests", "README.md", "LICENSE", ]
Package Data con Setuptools
[tool.setuptools.package-data] mi_paquete = [ "data/*.json", "templates/*.html", "static/**/*", ]
Acceder a los Recursos
from importlib.resources import files, as_file import json def load_config() -> dict: """Cargar archivo de configuración incluido en el paquete.""" # Forma moderna (Python 3.9+) config_file = files("mi_paquete.data").joinpath("config.json") with as_file(config_file) as path: with open(path) as f: return json.load(f) def read_template(name: str) -> str: """Leer plantilla HTML del paquete.""" template = files("mi_paquete.templates").joinpath(name) return template.read_text()
importlib.resources es la forma recomendada de acceder a recursos del paquete. Funciona correctamente incluso si el paquete está instalado en un archivo ZIP.
Resumen del Capítulo
01 // pyproject.toml
El estándar moderno para configurar proyectos Python con metadatos, dependencias y build system.
02 // Build Backends
Hatchling, Setuptools, Flit y otros transforman tu código en paquetes distribuibles.
03 // Entry Points
Expone comandos CLI y plugins que se instalan automáticamente con tu paquete.
04 // Wheels y Sdist
Formatos de distribución: wheels pre-compilados y source distributions universales.
05 // Publicación PyPI
Usa Twine para subir de forma segura a TestPyPI (pruebas) y PyPI (producción).
06 // Versionado Semántico
MAJOR.MINOR.PATCH comunica la naturaleza de los cambios a tus usuarios.