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 otro reservo ya existe, el tuyo no se puede subir con ese nombre. Es lo que se teclea en pip install <name>.
  • version — la versión (cápsula 05). Junto con name, 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 que pip no 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 METADATAEn la página de PyPI
name = "reservo"Name: reservoEl título del paquete y la URL
version = "0.1.0"Version: 0.1.0La 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 cuerpoLa descripción larga (el cuerpo de la página)
license = "MIT"License-Expression: MITLa licencia mostrada
keywords = [...]Keywords: booking,cli,coworkingPalabras 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.10El requisito que pip valida al instalar

Fíjate en tres cosas:

  1. El README se incrustó completo al final del METADATA, precedido de Description-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.
  2. La licencia salió como License-Expression: MIT — el formato moderno (SPDX, PEP 639) para expresar licencias. MIT es un identificador estándar que PyPI reconoce.
  3. 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 es text/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:

  1. 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".
  2. 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: agrega license = "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) y version (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, classifiers válidos y requires-python.
  • Todo eso ya lo declaraste en [project] (M2); el build backend lo traduce al archivo METADATA del wheel, que es lo que PyPI lee para armar la página. Lo comprobaste con unzip -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 check valida 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