Módulo 5: Entry points y distribución
7. Qué exige PyPI: metadata, README y license
Descripción de la cápsula
La cápsula anterior te mostró cómo se sube un paquete. Esta te muestra qué necesita tener para que el índice lo acepte y lo muestre bien. Cuando subes a PyPI, el índice no recibe solo tu código: recibe un paquete con metadata —un nombre, una versión, una descripción, un README, una licencia, unas etiquetas—, y esa metadata es lo que construye la página pública del paquete, lo que hace que aparezca en las búsquedas, y lo que le dice a quien lo instala qué es y bajo qué términos puede usarlo. Un paquete con metadata pobre se sube, pero se ve roto y no lo encuentra nadie.
Lo bueno: toda esa metadata ya la declaraste en el [project] de tu pyproject.toml (Módulo 2). Esta cápsula la mira desde el otro lado —el de PyPI— para que entiendas qué exige el índice, qué es obligatorio y qué es "muy recomendable", y cómo se ve tu declaración una vez convertida en la metadata real del paquete. Y lo comprobamos abriendo el METADATA del wheel que construiste: el mismo archivo que PyPI leería.
Conexión con el módulo: la cápsula 06 fue el flujo de subida; esta es el "checklist de admisión" del índice. Junto con el versionado (05), completan lo que necesitas saber para distribuir un paquete de verdad — aunque aquí no publiquemos.
Una analogía: la ficha de un producto en una tienda
Cuando un producto llega a la estantería de una tienda, no basta con la caja: necesita una ficha —nombre, código, descripción, precio, categoría—. Sin nombre no se puede pedir; sin código no se puede distinguir de otro; sin descripción el cliente no sabe qué es; sin categoría no aparece en el pasillo correcto. La tienda exige algunos de esos campos para siquiera aceptar el producto, y recomienda otros para que se venda.
La metadata de tu paquete es esa ficha. PyPI exige lo mínimo para identificarlo (name y version) y recomienda fuertemente el resto (descripción, README, license, classifiers) para que la página se vea completa y el paquete sea encontrable. Esta cápsula recorre la ficha campo por campo.
Lo que el índice exige y lo que recomienda
No todos los campos pesan igual. Se dividen en dos grupos:
Obligatorio (sin esto, el índice rechaza o no puede identificar el paquete):
name— el nombre del paquete. Debe ser único en el índice: si otroreservoya existe, el tuyo no se puede subir con ese nombre. Es lo que se teclea enpip install <name>.version— la versión (cápsula 05). Junto conname, identifica de forma única cada release. Una(name, version)se sube una sola vez.
Muy recomendado (el paquete se sube sin esto, pero se ve pobre o no lo encuentra nadie):
description— un resumen de una línea (el "summary"). Es lo que aparece bajo el nombre en los resultados de búsqueda de PyPI.readme— el archivo README, que se convierte en la descripción larga: el cuerpo de la página del paquete. Con su content-type (Markdown o reStructuredText) para que renderice bien.license— la licencia bajo la que se distribuye. Sin ella, legalmente nadie sabe si puede usar tu código.classifiers— etiquetas estándar (los "trove classifiers") que categorizan el paquete: versiones de Python soportadas, licencia, estado de desarrollo, entorno. Alimentan los filtros de búsqueda de PyPI.requires-python— qué versiones de Python soporta, para quepipno instale el paquete en un Python incompatible.
Todo esto lo declaraste en el [project] del pyproject.toml en M2. Vamos a verlo convertido en la metadata real.
Ejemplo trabajado: leer el METADATA del wheel
Cuando el build backend construye el wheel, traduce tu [project] a un archivo estándar llamado METADATA (formato de "core metadata"). Ese archivo es, esencialmente, lo que PyPI lee para armar la página. Ábrelo:
unzip -p dist/reservo-0.1.0-py3-none-any.whl 'reservo-0.1.0.dist-info/METADATA'
Qué esperar. La metadata de reservo en formato de cabeceras, seguida del README completo. Salida real:
Metadata-Version: 2.4
Name: reservo
Version: 0.1.0
Summary: Reserva de salas de coworking como libreria y CLI
Project-URL: Homepage, https://reservo.example
Project-URL: Repository, https://reservo.example/repo
Author-email: Equipo Reservo <dev@reservo.example>
License-Expression: MIT
Keywords: booking,cli,coworking
Classifier: Environment :: Console
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.10
Description-Content-Type: text/markdown
# reservo
Reserva de salas de coworking, empaquetada como librería y CLI.
reservo price --room Focus --tier basic --hours 3
# -> 7500
Recorramos cómo cada campo de tu [project] se volvió una cabecera de la ficha, y a dónde va en la página de PyPI:
Tu [project] (pyproject.toml) | Cabecera en METADATA | En la página de PyPI |
|---|---|---|
name = "reservo" | Name: reservo | El título del paquete y la URL |
version = "0.1.0" | Version: 0.1.0 | La versión mostrada; el selector de releases |
description = "..." | Summary: ... | El subtítulo bajo el nombre y en las búsquedas |
readme = "README.md" | Description-Content-Type: text/markdown + el cuerpo | La descripción larga (el cuerpo de la página) |
license = "MIT" | License-Expression: MIT | La licencia mostrada |
keywords = [...] | Keywords: booking,cli,coworking | Palabras clave para la búsqueda |
classifiers = [...] | Classifier: ... (una por línea) | Los filtros y etiquetas de la barra lateral |
requires-python = ">=3.10" | Requires-Python: >=3.10 | El requisito que pip valida al instalar |
Fíjate en tres cosas:
- El README se incrustó completo al final del
METADATA, precedido deDescription-Content-Type: text/markdown. Ese content-type es lo que le dice a PyPI "renderiza esto como Markdown", no como texto plano. Si lo declaras mal, la página muestra el README crudo. - La licencia salió como
License-Expression: MIT— el formato moderno (SPDX, PEP 639) para expresar licencias.MITes un identificador estándar que PyPI reconoce. - Los classifiers viajaron uno por línea. Cada
Classifier:es una etiqueta de un vocabulario cerrado (los trove classifiers); PyPI los usa para los filtros de la barra lateral y rechaza los inválidos.
Este METADATA es el que sube junto al wheel. Cuando PyPI recibe el paquete, lee este archivo y construye la página con él. Tu pyproject.toml es la fuente; este archivo es lo que el índice consume.
Profundización: twine check, classifiers y license
twine check valida que la descripción larga renderice
El riesgo más común de metadata es que el README no renderice en PyPI (se vea como texto plano o con errores). twine check (cápsula 06) valida exactamente eso, localmente, antes de subir:
twine check dist/*
Salida real:
Checking dist/reservo-0.1.0-py3-none-any.whl: PASSED
Checking dist/reservo-0.1.0.tar.gz: PASSED
PASSED significa que la metadata es válida y que la descripción larga renderizará bien. Si el Description-Content-Type estuviera mal o el README tuviera marcado inválido, verías FAILED con el detalle. Corre twine check siempre antes de pensar en subir; es local y gratis.
Los classifiers son un vocabulario cerrado
Los classifiers no son texto libre: son un conjunto fijo de valores válidos (los trove classifiers), publicados por PyPI. Algunos de los que usa reservo:
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Environment :: Console",
]
Si escribes un classifier que no está en la lista oficial (por ejemplo "Programming Language :: Python :: 3.99"), PyPI rechaza la subida con un error de "invalid classifier". Por eso conviene copiarlos de la lista oficial de classifiers de PyPI, no inventarlos. Sirven para que la gente filtre: "solo paquetes con licencia MIT", "solo los que soportan Python 3", "solo apps de consola".
La licencia: por qué importa y el formato moderno
Sin una licencia declarada, tu código no tiene términos de uso claros — legalmente, otros no saben si pueden usarlo. Declarar license = "MIT" (u otra) es lo que hace tu paquete usable por otros con confianza. El formato moderno (PEP 639) usa expresiones de licencia SPDX: identificadores estándar como MIT, Apache-2.0, GPL-3.0-or-later. En el METADATA lo viste como License-Expression: MIT. Es más preciso que el viejo campo de texto libre, y PyPI lo entiende y lo muestra como una etiqueta.
Errores comunes
Un classifier inválido que rechaza la subida (de datos). Qué pasa: escribes un classifier que no está en la lista oficial y el índice rechaza el paquete al subir con "invalid classifier". Cómo corregirlo: usa solo classifiers de la lista oficial de PyPI (cópialos, no los inventes). Un typo (Lisence en vez de License, o una versión de Python inexistente) es suficiente para el rechazo.
El README no renderiza porque el content-type está mal (de configuración). Qué pasa: subes y la página muestra el README como texto plano o con etiquetas crudas. Cómo corregirlo: el content-type debe corresponder al formato real del archivo (text/markdown para .md, text/x-rst para .rst). Los backends lo infieren de la extensión cuando usas readme = "README.md"; si lo declaras a mano, que coincida. Corre twine check para detectarlo antes de subir.
Olvidar la licencia (de omisión). Qué pasa: subes sin license, y aunque el paquete se instala, nadie sabe bajo qué términos puede usarlo — muchos evitarán un paquete sin licencia. Cómo corregirlo: declara siempre una licencia (license = "MIT" u otra expresión SPDX). Es una línea, y es la diferencia entre "código usable" y "código que legalmente nadie sabe si puede tocar".
Confundir el name del paquete con el nombre del módulo importable (de concepto). Qué pasa: crees que name en [project] tiene que ser igual al nombre con el que se importa. Cómo corregirlo: normalmente coinciden (reservo y import reservo), pero no es obligatorio: name es lo que se teclea en pip install y lo que ocupa el nombre único en PyPI; el nombre importable lo da la carpeta del paquete en src/. Pueden diferir (p. ej. pip install scikit-learn pero import sklearn). Lo que sí debe ser único en el índice es el name.
Ejercicios
Ejercicio 1: Mapea metadata a la página de PyPI (Fácil)
Para cada campo, di qué parte de la página de PyPI construye: description, readme, classifiers.
Ver solución
description→ el subtítulo de una línea que aparece bajo el nombre del paquete, y el texto que se ve en los resultados de búsqueda. Es el "summary".readme→ la descripción larga: el cuerpo principal de la página del paquete, renderizado (como Markdown, si el content-type estext/markdown).classifiers→ las etiquetas y filtros de la barra lateral (licencia, versiones de Python, estado, entorno), que alimentan la búsqueda por categorías de PyPI.
Los tres salen de tu [project] y terminan en el METADATA del wheel, que PyPI lee para armar la página.
Ejercicio 2: Inspecciona el METADATA y verifica campos (Medio)
Abre el METADATA del wheel de reservo y confirma tres cosas: el Name, el License-Expression, y que el Description-Content-Type es text/markdown.
Ver solución
unzip -p dist/reservo-0.1.0-py3-none-any.whl 'reservo-0.1.0.dist-info/METADATA' | grep -E '^(Name|License-Expression|Description-Content-Type):'
Salida real:
Name: reservo
License-Expression: MIT
Description-Content-Type: text/markdown
Explicación: los tres campos vinieron de tu [project]: name = "reservo" → Name: reservo; license = "MIT" → License-Expression: MIT (formato SPDX moderno); y readme = "README.md" hizo que el backend infiriera Description-Content-Type: text/markdown, que es lo que le dice a PyPI que renderice el README como Markdown. Verificar esto antes de subir evita una página rota.
Ejercicio 3: Detecta el problema en un [project] (Medio)
Este [project] haría que la subida a PyPI falle o se vea mal. Encuentra los dos problemas:
[project]
name = "reservo"
version = "0.1.0"
readme = "README.md"
classifiers = [
"Programming Language :: Python :: 3.99",
]
Ver solución
Dos problemas:
- Classifier inválido.
"Programming Language :: Python :: 3.99"no existe en la lista oficial de trove classifiers (no hay Python 3.99). PyPI rechazaría la subida con "invalid classifier". Corrección: usa uno válido, como"Programming Language :: Python :: 3"o"Programming Language :: Python :: 3.14". - Falta la licencia. No hay campo
license. El paquete se subiría, pero sin términos de uso claros — muchos lo evitarían. Corrección: agregalicense = "MIT"(o la expresión SPDX que corresponda).
Corregido:
[project]
name = "reservo"
version = "0.1.0"
readme = "README.md"
license = "MIT"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
]
Un twine check habría alertado del content-type y la metadata; el classifier inválido lo rechaza el propio índice al subir. Por eso se valida antes.
Resumen y siguiente paso
En esta cápsula viste el "checklist de admisión" del índice:
- PyPI exige lo mínimo para identificar el paquete:
name(único en el índice) yversion(una(name, version)se sube una sola vez). Sin eso, no hay paquete. - Recomienda fuertemente el resto para que la página se vea completa y sea encontrable:
description(summary), el README como descripción larga (con su content-type),license,classifiersválidos yrequires-python. - Todo eso ya lo declaraste en
[project](M2); el build backend lo traduce al archivoMETADATAdel wheel, que es lo que PyPI lee para armar la página. Lo comprobaste conunzip -p. - Detalles que importan: el content-type del README (para que renderice), los classifiers de un vocabulario cerrado (un typo rechaza la subida), y la licencia en formato SPDX (
License-Expression: MIT). twine checkvalida localmente toda esta metadata y que el README renderice, antes de subir. Es el filtro previo, y sí se ejecuta.
Siguiente: 08-mini-project-ship-reservo-as-a-command.md — El mini-proyecto: declaras el entry point, instalas reservo, corres el comando (7500 / 6000), lees el wrapper, y dejas escrito el flujo twine upload a TestPyPI como contenido. El capstone del módulo.
Recursos
- Core metadata specifications (packaging.python.org) — El estándar del archivo
METADATA: cada cabecera, cuáles son obligatorias y cuáles opcionales. - Writing your
pyproject.toml(packaging.python.org) — La referencia campo por campo del[project]:readme,license,classifiers,keywords. - PyPI classifiers list — La lista oficial y completa de trove classifiers válidos, para copiar en vez de inventar.
- License expression (PEP 639, packaging.python.org) — El formato SPDX moderno de licencias que viste como
License-Expression: MIT.