Módulo 2 · Lección 22 de 28
Distribuir tu herramienta
Cómo llega tu programa al ordenador de otra persona: instalación desde git, pipx, construcción de paquetes y publicación en PyPI.
Tu herramienta se instala con pip install -e . y funciona. Pero esa orden solo la puedes ejecutar tú, porque necesita la carpeta del proyecto delante. Para que la use otra persona hay que responder a una pregunta nueva: cómo consigue esa persona el código y qué escribe para tenerlo funcionando.
Hay tres respuestas, de menos a más formal: le pasas la carpeta, le pasas la dirección del repositorio o lo publicas en el índice público de paquetes. Las tres usan las mismas herramientas y el mismo pyproject.toml que ya tienes.
Instalar desde una carpeta#
Es lo mismo que hiciste en la lección anterior, pero sin la -e:
pip install /Users/erwin/proyectos/misgastosSin -e, pip construye el paquete y copia el resultado dentro del entorno. A partir de ese momento las dos cosas son independientes: puedes borrar la carpeta del proyecto y el comando seguirá funcionando.
misgastos resumencomida 55.80 €
ocio 18.00 €
transporte 2.40 €Esta es la forma de probar que tu proyecto se instala de verdad, y conviene hacerlo en un entorno virtual limpio antes de dárselo a nadie. Si te has dejado un módulo fuera del paquete o el punto de entrada está mal, aquí se ve; con la instalación editable, no.
Instalar desde un repositorio de git#
pip sabe clonar. Le das la dirección del repositorio con el prefijo git+ y él se encarga:
pip install git+https://github.com/erwin/misgastos.gitBuilding wheels for collected packages: misgastos
Building wheel for misgastos (pyproject.toml): started
Building wheel for misgastos (pyproject.toml): finished with status 'done'
Created wheel for misgastos: filename=misgastos-0.1.0-py3-none-any.whl size=5132
Successfully built misgastos
Installing collected packages: misgastos
Successfully installed misgastos-0.1.0Clona en una carpeta temporal, encuentra el pyproject.toml, construye el paquete, lo instala y borra la copia. Quien lo ejecute no necesita saber nada de tu proyecto más allá de la dirección.
Se puede pedir una rama, una etiqueta o un commit concreto añadiendo @ al final:
pip install git+https://github.com/erwin/misgastos.git@v0.2.0
pip install git+https://github.com/erwin/misgastos.git@desarrolloEsta es la forma habitual de compartir algo que todavía no está publicado, o que nunca lo estará porque solo lo usáis en tu equipo.
pipx: la forma correcta de instalar herramientas#
Aquí hay un problema que todavía no hemos mirado de frente. Una herramienta de línea de comandos no es una biblioteca: no la vas a importar desde tus programas, la vas a ejecutar. Y quieres ejecutarla desde cualquier carpeta, sin acordarte de activar ningún entorno.
La tentación es instalarla en el Python del sistema:
pip install --user misgastosY ahí empiezan los problemas que ya conoces de la lección de entornos virtuales, multiplicados. Si misgastos necesita rich>=13 y otra herramienta que instalaste el año pasado necesita rich<13, una de las dos se rompe. Cada herramienta que instalas así ensucia un poco más un entorno compartido que nadie controla, y tarde o temprano una actualización deja algo inservible.
pipx resuelve esto con una idea muy simple: crea un entorno virtual por herramienta y deja solo el comando a la vista. Cada programa tiene sus dependencias aisladas, y tú escribes su nombre desde cualquier sitio sin activar nada.
Instalar pipx#
python3 -m pip install --user pipx
python3 -m pipx ensurepathensurepath añade a tu PATH la carpeta donde pipx deja los comandos. Hay que abrir una terminal nueva para que el cambio surta efecto. En macOS con Homebrew, brew install pipx hace las dos cosas.
pipx --version1.17.4Instalar una herramienta#
Admite lo mismo que pip: un nombre del índice público, una carpeta o una dirección de git con git+. Como misgastos todavía no está publicado, se instala desde su carpeta:
pipx install /Users/erwin/proyectos/misgastoscreating virtual environment...
determining package name from '/Users/erwin/proyectos/misgastos'...
creating virtual environment...
installing misgastos from spec '/Users/erwin/proyectos/misgastos'...
done! ✨ 🌟 ✨
installed package misgastos 0.1.0, installed using Python 3.14.7
These apps are now available
- misgastosmisgastos resumencomida 55.80 €
ocio 18.00 €
transporte 2.40 €Y ahí está lo importante: eso funciona con cualquier entorno virtual activado y sin ninguno.
Las órdenes de pipx#
| Orden | Qué hace |
|---|---|
pipx install NOMBRE | Instala la herramienta en su propio entorno |
pipx list | Enseña lo instalado y qué comandos expone cada cosa |
pipx list --short | Lo mismo, una línea por herramienta |
pipx upgrade NOMBRE | Actualiza una |
pipx upgrade-all | Actualiza todas |
pipx uninstall NOMBRE | La borra con su entorno |
pipx reinstall-all | Rehace todos los entornos, útil al cambiar de versión de Python |
pipx run NOMBRE | La ejecuta sin instalarla, descargándola a un caché temporal |
pipx runpip NOMBRE pip ... | Ejecuta pip dentro del entorno de esa herramienta |
Ver lo que hay instalado:
pipx listvenvs are in /Users/erwin/.local/share/pipx/venvs
apps are exposed on your $PATH at /Users/erwin/.local/bin
manual pages are exposed at /Users/erwin/.local/share/man
shell completions are exposed at /Users/erwin/.local/share
package misgastos 0.1.0, installed using Python 3.14.7
- misgastosActualizar, después de subir la versión a 0.2.0 en el pyproject.toml y en el __init__.py:
pipx upgrade misgastosupgrading shared libraries...
upgrading misgastos from spec '/Users/erwin/proyectos/misgastos'...
upgraded package misgastos from 0.1.0 to 0.2.0 (location:
/Users/erwin/.local/share/pipx/venvs/misgastos)Mirar qué hay dentro del entorno de una herramienta, sin activarlo:
pipx runpip misgastos listPackage Version
--------- -------
misgastos 0.2.0
pip 26.2.1Y desinstalar, que se lleva por delante el entorno entero:
pipx uninstall misgastosuninstalled misgastos! ✨ 🌟 ✨Construir los paquetes distribuibles#
Hasta ahora la instalación partía de tu carpeta o de un repositorio, y el paquete se construía sobre la marcha en el ordenador de destino. Para publicar hay que construirlo antes y subir el resultado.
La herramienta se llama build y se instala en el entorno virtual:
pip install buildDesde la raíz del proyecto:
python -m buildTarda unos segundos y termina así:
Successfully built misgastos-0.1.0.tar.gz and misgastos-0.1.0-py3-none-any.whlEn una carpeta dist/ nueva han aparecido dos ficheros:
ls -l dist-rw-r--r-- 1 erwin staff 5132 19 sep 14:20 misgastos-0.1.0-py3-none-any.whl
-rw-r--r-- 1 erwin staff 5526 19 sep 14:20 misgastos-0.1.0.tar.gzSon las dos formas en que viaja un paquete Python, y conviene entender en qué se diferencian.
El .whl, la rueda#
Es un fichero ZIP con el paquete ya construido, listo para copiar. Instalarlo es descomprimirlo en el sitio correcto y escribir los ejecutables: no se ejecuta código tuyo, no se compila nada y tarda un instante.
unzip -l dist/misgastos-0.1.0-py3-none-any.whlArchive: dist/misgastos-0.1.0-py3-none-any.whl
Length Date Time Name
--------- ---------- ----- ----
89 09-19-2026 20:17 misgastos/__init__.py
121 09-19-2026 20:18 misgastos/__main__.py
836 09-19-2026 20:17 misgastos/almacen.py
3116 09-19-2026 20:18 misgastos/cli.py
1663 09-19-2026 20:17 misgastos/modelo.py
185 09-19-2026 20:20 misgastos-0.1.0.dist-info/licenses/LICENSE
571 09-19-2026 20:20 misgastos-0.1.0.dist-info/METADATA
91 09-19-2026 20:20 misgastos-0.1.0.dist-info/WHEEL
49 09-19-2026 20:20 misgastos-0.1.0.dist-info/entry_points.txt
10 09-19-2026 20:20 misgastos-0.1.0.dist-info/top_level.txt
878 09-19-2026 20:20 misgastos-0.1.0.dist-info/RECORD
--------- -------
7609 11 filesFíjate en dos cosas. La primera: la carpeta src/ ha desaparecido. Dentro de la rueda el paquete está en la raíz, porque src/ solo servía para organizar el proyecto, no para instalarlo. La segunda: ahí está el punto de entrada que escribiste, convertido en un fichero de texto:
unzip -p dist/misgastos-0.1.0-py3-none-any.whl misgastos-0.1.0.dist-info/entry_points.txt[console_scripts]
misgastos = misgastos.cli:mainEl nombre del fichero también dice cosas: misgastos-0.1.0-py3-none-any.whl significa nombre, versión, cualquier Python 3, ninguna ABI concreta y cualquier sistema operativo. Un paquete con código en C tendría ahí algo como cp313-cp313-macosx_11_0_arm64, y habría una rueda distinta por cada combinación.
El .tar.gz, la distribución de fuentes#
Es un comprimido con el proyecto tal como lo escribiste, incluidas las cosas que no se instalan:
tar tzf dist/misgastos-0.1.0.tar.gzmisgastos-0.1.0/
misgastos-0.1.0/LICENSE
misgastos-0.1.0/PKG-INFO
misgastos-0.1.0/README.md
misgastos-0.1.0/pyproject.toml
misgastos-0.1.0/setup.cfg
misgastos-0.1.0/src/
misgastos-0.1.0/src/misgastos/
misgastos-0.1.0/src/misgastos/__init__.py
misgastos-0.1.0/src/misgastos/__main__.py
misgastos-0.1.0/src/misgastos/almacen.py
misgastos-0.1.0/src/misgastos/cli.py
misgastos-0.1.0/src/misgastos/modelo.py
misgastos-0.1.0/tests/
misgastos-0.1.0/tests/test_almacen.py
misgastos-0.1.0/tests/test_cli.py
misgastos-0.1.0/tests/test_modelo.pyAquí sí está src/, está el pyproject.toml y están las pruebas. Para instalarlo hay que construirlo primero, así que es más lento.
.whl | .tar.gz | |
|---|---|---|
| Qué contiene | El paquete construido | El proyecto entero |
| Al instalar | Se descomprime y ya | Hay que construirlo antes |
| Velocidad | Inmediata | Segundos o minutos |
Lleva src/, pruebas y configuración | No | Sí |
| Sirve para compilar en otra plataforma | No | Sí |
Se publican los dos. pip usará la rueda siempre que pueda, y recurrirá al .tar.gz cuando no haya una rueda compatible con ese sistema. Y python -m build los genera a la vez precisamente porque lo normal es subir ambos.
Publicar en PyPI#
PyPI es el índice público de paquetes de Python: el sitio del que descarga pip install cuando le das un nombre a secas. Publicar ahí significa que cualquiera en el mundo puede instalar tu herramienta escribiendo su nombre.
La herramienta para subir se llama twine:
pip install twineAntes de subir: comprobar#
twine sabe revisar los ficheros construidos y avisar de los problemas que rechazaría el índice, sobre todo un README.md que no se renderiza bien:
twine check dist/*Checking dist/misgastos-0.1.0-py3-none-any.whl: PASSED
Checking dist/misgastos-0.1.0.tar.gz: PASSEDTestPyPI: el índice de pruebas#
Hay una copia completa de PyPI para practicar, con su propio registro de usuarios y sus propios nombres de paquete. Se llama TestPyPI y sirve exactamente para lo que suena: equivocarse sin consecuencias.
twine upload --repository testpypi dist/*Te pedirá credenciales, que se obtienen creando una cuenta en test.pypi.org y generando allí un token. Una vez subido, se puede probar la instalación de verdad apuntando pip a ese índice:
pipx install --index-url https://test.pypi.org/simple/ misgastosHazlo al menos una vez antes de tu primera publicación real. Es la única manera de ver el aspecto que tendrá la página de tu proyecto y de comprobar que la instalación desde un índice funciona.
El índice de verdad#
twine upload dist/*Y con eso, cualquiera puede hacer:
pipx install misgastosVersionado semántico#
El número de versión no es un contador. La convención que sigue casi todo el ecosistema se llama versionado semántico y tiene tres números separados por puntos:
MAYOR.MENOR.PARCHECada uno se sube por una razón distinta:
| Número | Cuándo sube | Ejemplo en misgastos |
|---|---|---|
| PARCHE | Arreglas un fallo sin cambiar nada más | El total sumaba mal con la categoría vacía |
| MENOR | Añades algo nuevo sin romper lo anterior | Un subcomando exportar |
| MAYOR | Cambias algo que rompe el uso actual | anadir pasa a exigir la categoría |
Y la regla que lo ata todo: al subir un número, los de su derecha se ponen a cero. De la 1.4.7, un arreglo lleva a la 1.4.8, una función nueva a la 1.5.0 y un cambio incompatible a la 2.0.0.
Lo que cuenta como "romper" es cualquier cosa que obligue a quien usa tu herramienta a cambiar lo que hacía: quitar un subcomando, renombrar una opción, cambiar el formato del fichero de datos o modificar un código de salida. Si alguien tenía un script que llamaba a tu comando y ese script deja de funcionar, es un cambio mayor.
Lista de comprobación antes de publicar#
Repásala entera cada vez. Ocupa cinco minutos y evita una versión quemada.
- Las pruebas pasan.
pytesten verde, que es lo de la lección siguiente. - El número de versión está subido en los dos sitios,
pyproject.tomly__init__.py, y el salto es el que corresponde según el versionado semántico. - El
README.mdestá al día. Es la portada pública: si documenta un subcomando que ya no existe, se nota enseguida. - El
LICENSEestá en la raíz y el campolicensedelpyproject.tomldice lo mismo. dependenciesestá completo. Instala el proyecto en un entorno virtual recién creado y ejecútalo: si falta una biblioteca, ahí saltará elModuleNotFoundError.requires-pythones cierto. No declares 3.9 si nunca lo has probado con 3.9.- Borra
dist/y reconstruye.rm -rf dist buildy despuéspython -m build, para no subir restos de versiones anteriores. twine check dist/*pasa.- Prueba la instalación desde el fichero construido, no desde la carpeta:
pip install dist/misgastos-0.1.0-py3-none-any.whlen un entorno limpio, y ejecuta el comando. - Sube primero a TestPyPI si es tu primera vez o si has tocado el
pyproject.toml. - Pon una etiqueta en git con la versión,
git tag v0.1.0, para poder volver exactamente a lo que publicaste.