Para crear y publicar un paquete Python moderno en PyPI, organiza el código con un layout src, define el proyecto y el sistema de construcción en pyproject.toml, genera una wheel y una sdist mediante python -m build, compruébalas con twine check y ensaya primero en TestPyPI. En producción, conecta PyPI con GitHub Actions mediante Trusted Publishing para no guardar tokens permanentes.
Esta guía lleva un paquete pequeño, saludo-claro, desde un directorio vacío hasta un release repetible. Si necesitas refrescar conceptos, consulta qué son los módulos y paquetes de Python, trabaja en un entorno virtual y revisa la gestión de dependencias con pip.
Paquete de importación y distribución no son lo mismo
En packaging, la palabra “paquete” se usa para objetos relacionados, pero distintos. Un paquete de importación es el directorio que Python encuentra al ejecutar import saludo_claro. Una distribución es el archivo instalable alojado en un índice: por ejemplo, saludo_claro-0.1.0-py3-none-any.whl o un archivo fuente .tar.gz.
El nombre de distribución puede llevar guion, mientras que el import debe ser un identificador Python válido. El usuario ejecuta pip install saludo-claro y luego escribe from saludo_claro import saludar. Tampoco conviene confundir PyPI con pip: PyPI aloja e indexa distribuciones; pip las resuelve e instala.
Estructura mantenible con layout src
saludo-claro/
├── .github/
│ └── workflows/
│ └── publish.yml
├── src/
│ └── saludo_claro/
│ ├── __init__.py
│ └── core.py
├── tests/
│ └── test_core.py
├── LICENSE
├── README.md
└── pyproject.toml
El layout src evita que las pruebas importen por accidente el código directamente desde la raíz. Para probar hay que instalar el proyecto, lo que revela fallos de configuración y se aproxima mejor a la experiencia real del usuario. Además, separa el código importable de documentación, automatización y otros archivos.
__init__.py declara un paquete regular y puede presentar su API pública. Deja tests, README y workflows fuera de src. Cuando el proyecto crezca, los subpaquetes vivirán bajo src/saludo_claro/.
Un pyproject.toml actual con setuptools
pyproject.toml reúne cómo construir la distribución y qué metadatos incluir. La especificación oficial de pyproject.toml diferencia las tablas estándar de la configuración propia de cada herramienta.
[build-system]
requires = ["setuptools>=77.0.3"]
build-backend = "setuptools.build_meta"
[project]
name = "saludo-claro"
version = "0.1.0"
description = "Saludos pequeños y predecibles para ejemplos en Python"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
license-files = ["LICENSE"]
authors = [
{ name = "Tu Nombre", email = "[email protected]" },
]
keywords = ["saludo", "ejemplo", "tutorial"]
classifiers = [
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
]
dependencies = []
[project.optional-dependencies]
test = ["pytest>=8"]
[project.urls]
Homepage = "https://github.com/tu-usuario/saludo-claro"
Issues = "https://github.com/tu-usuario/saludo-claro/issues"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
[build-system] indica al frontend que prepare setuptools en un entorno de construcción aislado. La tabla [project] emplea metadatos que comprenden los backends compatibles. Sustituye identidad, descripción y URLs, y busca primero un nombre de distribución libre en PyPI. requires-python debe representar las versiones que pruebas de verdad, no solo la instalada en tu equipo.
Las bibliotecas necesarias al ejecutar el paquete van en dependencies. Las herramientas exclusivas de desarrollo no deberían instalarse a todos los consumidores; por eso pytest está en el extra test. Para documentar mejor los contratos públicos, profundiza en type hints de Python.
README y licencia también son parte del producto
PyPI muestra el README.md declarado en la página del proyecto. Explica en pocas líneas qué resuelve la biblioteca, cómo se instala y cuál es el uso mínimo. Usa URLs absolutas para imágenes: una ruta relativa válida en GitHub puede romperse en PyPI.
## saludo-claro
Una biblioteca pequeña para producir saludos consistentes.
## Instalación
`python -m pip install saludo-claro`
## Uso
```python
from saludo_claro import saludar
print(saludar("Ada"))
```
Elige conscientemente una licencia e incluye su texto completo en LICENSE. El ejemplo declara la expresión SPDX MIT; cámbiala si no refleja tu decisión. Que el código sea visible no otorga por sí solo permiso para copiarlo, modificarlo y redistribuirlo.
Código mínimo y pruebas sobre la API pública
Crea src/saludo_claro/core.py:
def saludar(nombre: str) -> str:
"""Devuelve un saludo para un nombre no vacío."""
nombre_limpio = nombre.strip()
if not nombre_limpio:
raise ValueError("el nombre no puede estar vacío")
return f"¡Hola, {nombre_limpio}!"
Expón el contrato previsto desde src/saludo_claro/__init__.py:
from .core import saludar
__all__ = ["saludar"]
Añade tests/test_core.py:
import pytest
from saludo_claro import saludar
def test_saludar_elimina_espacios() -> None:
assert saludar(" Ada ") == "¡Hola, Ada!"
def test_saludar_rechaza_nombre_vacio() -> None:
with pytest.raises(ValueError, match="no puede estar vacío"):
saludar(" ")
Dentro del entorno virtual activado, instala el proyecto en modo editable con el extra de pruebas:
python -m pip install --upgrade pip
python -m pip install -e ".[test]"
python -m pytest
La instalación editable refleja cambios en el código sin reinstalar manualmente, pero mantiene el proyecto instalado y su importación realista. La guía de pruebas automatizadas con pytest desarrolla fixtures, organización y cobertura.
Construir wheel y sdist y validarlas
python -m pip install --upgrade build twine
python -m build
python -m twine check dist/*
La construcción aislada produce una wheel Python pura, como saludo_claro-0.1.0-py3-none-any.whl, y una sdist terminada en .tar.gz. La wheel ya está construida y suele instalarse rápido. La sdist contiene fuentes y se construye en el entorno de destino. Publica ambas para servir la wheel cuando sea posible sin perder la alternativa fuente.
twine check detecta metadatos defectuosos y problemas de renderizado del README, pero no demuestra que el import funcione. Instala la wheel por su ruta en otro entorno y realiza un smoke test. Elimina artefactos antiguos de dist antes de construir cada release. El proceso coincide con el tutorial oficial de packaging.
Ensayar la subida en TestPyPI
TestPyPI es un servicio independiente, con cuentas, proyectos y credenciales propios. Crea allí un token de API para el ensayo manual y sube los artefactos:
python -m twine upload --repository testpypi dist/*
Cuando Twine pregunte, usa __token__ como usuario y el token completo como contraseña. Nunca lo confirmes en Git ni lo escribas directamente en un comando que quede registrado. Verifica desde un entorno nuevo:
python -m pip install --index-url https://test.pypi.org/simple/ --no-deps saludo-claro
python -c "from saludo_claro import saludar; print(saludar('Ada'))"
--no-deps es correcto porque este ejemplo no tiene dependencias de ejecución. En proyectos reales, TestPyPI quizá no contenga todas. Instálalas por separado en vez de combinar índices sin cuidado, práctica que puede abrir la puerta a dependency confusion.
Publicar desde GitHub Actions sin tokens
Trusted Publishing intercambia una identidad OIDC de GitHub por una credencial breve de PyPI. Configura en PyPI un publisher con propietario, repositorio, nombre exacto del workflow publish.yml y environment pypi. Puede asociarse a un proyecto existente o prepararse como publisher pendiente para el primero. La documentación de Trusted Publishers de PyPI explica la configuración vigente.
Crea .github/workflows/publish.yml:
name: Publish package
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: python -m pip install --upgrade build
- run: python -m build
- uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/
publish:
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/saludo-claro
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
El permiso limitado id-token: write permite solicitar la identidad OIDC; no concede escritura general en el repositorio. Si necesitas control humano, añade revisores al environment pypi. La CI habitual debería probar pushes y pull requests, mientras este workflow publica únicamente al crear un GitHub Release. Consulta la guía de CI/CD de Python con GitHub Actions para separar ambos procesos.
Versionado y rutina de release
PyPI no permite sobrescribir los archivos de una versión existente. Una convención práctica es MAJOR.MINOR.PATCH: PATCH para correcciones compatibles, MINOR para nuevas funciones compatibles y MAJOR para cambios incompatibles. Una versión candidata puede ser 0.2.0rc1. Sea cual sea tu política, la versión de pyproject.toml, la etiqueta Git y las notas deben contar la misma historia.
- Actualiza código, documentación, changelog y versión.
- Ejecuta tests y el comprobador de tipos, si el proyecto lo usa.
- Construye desde cero y ejecuta
twine check. - Instala la wheel en un entorno limpio y comprueba el import.
- Crea la etiqueta y publica un GitHub Release para activar el workflow.
Errores frecuentes al empaquetar y publicar
- Nombre ocupado: el nombre de distribución debe ser único en el índice, aunque el import tenga otro nombre.
- Paquete ausente en la wheel: revisa el directorio bajo
src,__init__.pyy el descubrimiento de setuptools. - Descripción rota: ejecuta
twine checky elimina markup no admitido o recursos locales. - Archivos inesperados: inspecciona los dos artefactos;
.gitignoreno define el contenido distribuido. - Subida rechazada: incrementa la versión y vuelve a construir; no intentes sustituir archivos publicados.
- Workflow no confiable: propietario, repositorio, archivo y environment deben coincidir exactamente con el registro de PyPI.
Checklist previo al release
- Nombre disponible y metadatos correctos.
- Paquete bajo
srcinstalable y API pública importable. - README renderizable, licencia incluida y URLs válidas.
- Versiones de Python declaradas cubiertas por CI.
- Pruebas verdes en un entorno limpio.
- Wheel y sdist nuevas, inspeccionadas y aprobadas por
twine check. - Instalación e import verificados mediante TestPyPI.
- Trusted Publisher y environment coincidentes, sin token persistente.
- Versión, etiqueta, changelog y notas de release coherentes.
Un buen packaging es más que conseguir que una subida termine sin errores. Proporciona instalación predecible, metadatos útiles, permisos claros y un proceso repetible sin improvisar en cada release. Esta base sirve tanto para una biblioteca didáctica pequeña como para un proyecto futuro con múltiples módulos y colaboradores.