Saltar al contenido
🐍 Curso de Python
/

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.

⏱ 22 min de lectura

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:

bash
pip install /Users/erwin/proyectos/misgastos

Sin -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.

bash
misgastos resumen
Salida en pantalla
comida            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:

bash
pip install git+https://github.com/erwin/misgastos.git
Salida en pantalla
Building 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.0

Clona 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:

bash
pip install git+https://github.com/erwin/misgastos.git@v0.2.0
pip install git+https://github.com/erwin/misgastos.git@desarrollo

Esta 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:

bash
pip install --user misgastos

Y 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#

bash
python3 -m pip install --user pipx
python3 -m pipx ensurepath

ensurepath 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.

bash
pipx --version
Salida en pantalla
1.17.4

Instalar 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:

bash
pipx install /Users/erwin/proyectos/misgastos
Salida en pantalla
creating 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
    - misgastos
bash
misgastos resumen
Salida en pantalla
comida            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#

OrdenQué hace
pipx install NOMBREInstala la herramienta en su propio entorno
pipx listEnseña lo instalado y qué comandos expone cada cosa
pipx list --shortLo mismo, una línea por herramienta
pipx upgrade NOMBREActualiza una
pipx upgrade-allActualiza todas
pipx uninstall NOMBRELa borra con su entorno
pipx reinstall-allRehace todos los entornos, útil al cambiar de versión de Python
pipx run NOMBRELa 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:

bash
pipx list
Salida en pantalla
venvs 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
    - misgastos

Actualizar, después de subir la versión a 0.2.0 en el pyproject.toml y en el __init__.py:

bash
pipx upgrade misgastos
Salida en pantalla
upgrading 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:

bash
pipx runpip misgastos list
Salida en pantalla
Package   Version
--------- -------
misgastos 0.2.0
pip       26.2.1

Y desinstalar, que se lleva por delante el entorno entero:

bash
pipx uninstall misgastos
Salida en pantalla
uninstalled 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:

bash
pip install build

Desde la raíz del proyecto:

bash
python -m build

Tarda unos segundos y termina así:

Salida en pantalla
Successfully built misgastos-0.1.0.tar.gz and misgastos-0.1.0-py3-none-any.whl

En una carpeta dist/ nueva han aparecido dos ficheros:

bash
ls -l dist
Salida en pantalla
-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.gz

Son 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.

bash
unzip -l dist/misgastos-0.1.0-py3-none-any.whl
Salida en pantalla
Archive:  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 files

Fí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:

bash
unzip -p dist/misgastos-0.1.0-py3-none-any.whl misgastos-0.1.0.dist-info/entry_points.txt
Salida en pantalla
[console_scripts]
misgastos = misgastos.cli:main

El 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:

bash
tar tzf dist/misgastos-0.1.0.tar.gz
Salida en pantalla
misgastos-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.py

Aquí 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é contieneEl paquete construidoEl proyecto entero
Al instalarSe descomprime y yaHay que construirlo antes
VelocidadInmediataSegundos o minutos
Lleva src/, pruebas y configuraciónNoSí
Sirve para compilar en otra plataformaNoSí

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:

bash
pip install twine

Antes 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:

bash
twine check dist/*
Salida en pantalla
Checking dist/misgastos-0.1.0-py3-none-any.whl: PASSED
Checking dist/misgastos-0.1.0.tar.gz: PASSED

TestPyPI: 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.

bash
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:

bash
pipx install --index-url https://test.pypi.org/simple/ misgastos

Hazlo 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#

bash
twine upload dist/*

Y con eso, cualquiera puede hacer:

bash
pipx install misgastos

Versionado 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:

text
MAYOR.MENOR.PARCHE

Cada uno se sube por una razón distinta:

NúmeroCuándo subeEjemplo en misgastos
PARCHEArreglas un fallo sin cambiar nada másEl total sumaba mal con la categoría vacía
MENORAñades algo nuevo sin romper lo anteriorUn subcomando exportar
MAYORCambias algo que rompe el uso actualanadir 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.

  1. Las pruebas pasan. pytest en verde, que es lo de la lección siguiente.
  2. El número de versión está subido en los dos sitios, pyproject.toml y __init__.py, y el salto es el que corresponde según el versionado semántico.
  3. El README.md está al día. Es la portada pública: si documenta un subcomando que ya no existe, se nota enseguida.
  4. El LICENSE está en la raíz y el campo license del pyproject.toml dice lo mismo.
  5. dependencies está completo. Instala el proyecto en un entorno virtual recién creado y ejecútalo: si falta una biblioteca, ahí saltará el ModuleNotFoundError.
  6. requires-python es cierto. No declares 3.9 si nunca lo has probado con 3.9.
  7. Borra dist/ y reconstruye. rm -rf dist build y después python -m build, para no subir restos de versiones anteriores.
  8. twine check dist/* pasa.
  9. Prueba la instalación desde el fichero construido, no desde la carpeta: pip install dist/misgastos-0.1.0-py3-none-any.whl en un entorno limpio, y ejecuta el comando.
  10. Sube primero a TestPyPI si es tu primera vez o si has tocado el pyproject.toml.
  11. Pon una etiqueta en git con la versión, git tag v0.1.0, para poder volver exactamente a lo que publicaste.