Skip to content

📙 Clase 34 — Empaquetar y distribuir: PyInstaller

Fase 6 · Especialización: Apps de escritorio 🎯 ⬅️ Volver al índice de clases

🎯 Qué aprendí

  • Convertir mi app en un ejecutable (.app en macOS / .exe en Windows) que corre sin Python instalado.
  • Los flags esenciales: --windowed, --onefile, --name, --icon.
  • El gotcha específico de CustomTkinter (--collect-all) y dónde guardar los datos.

📖 PARTE TEÓRICA

📦 1. ¿Qué hace PyInstaller?

Analiza tu main.py, junta el intérprete de Python + tus módulos + las librerías, y lo empaqueta en un ejecutable. El usuario final hace doble clic — no sabe qué es pip.

bash
pip install pyinstaller        # en tu venv (instalado: 6.21.0 ✔)
pyinstaller --version          # 6.21.0

🚀 2. El comando para una app GUI

bash
pyinstaller --windowed --name "MiAgenda" --collect-all customtkinter main.py
FlagQué hace (del --help real de 6.21.0)¿Cuándo?
-w, --windowed, --noconsolesin ventana de consola negra detrássiempre en apps GUI
-n, --name NAMEnombre del ejecutable y la carpetasiempre
--collect-all customtkinterincluye TODOS los archivos del paqueteCTk lo necesita (ver §3)
-F, --onefileun único archivo ejecutabledistribución simple (arranca más lento)
-i, --icon archivo.icns (macOS) / .ico (Windows)toque final
-y, --noconfirmsobreescribe dist/ sin preguntaral re-empaquetar

Resultado:

tu_proyecto/
├── build/            🗑️ archivos temporales (ignorar)
├── dist/
│   └── MiAgenda.app  🎉 LO QUE DISTRIBUYES (macOS; en Windows: MiAgenda.exe)
└── MiAgenda.spec     📋 la "receta" (edítala para builds avanzados y re-usa: pyinstaller MiAgenda.spec)

⚠️ 3. Los gotchas que te van a pasar

① CustomTkinter y sus assets. CTk trae archivos .json de temas e imágenes que PyInstaller no detecta solo (solo sigue imports de Python). Sin --collect-all customtkinter, la app empaquetada muere al abrir con FileNotFoundError de un tema.

📌 Es el problema #1 documentado en la guía oficial de empaquetado de CustomTkinter. Si también usas imágenes propias: --add-data "imagenes:imagenes".

② Los datos NO van junto al ejecutable. Dentro de un .app/onefile, la carpeta del programa es de solo lectura (o temporal). Si guardabas app.db "al lado del .py", empaquetado falla. La solución ya la aprendiste en la Clase 22:

python
RUTA_DATOS = Path.home() / ".mi_agenda"      # SIEMPRE en el home del usuario

③ Empaqueta desde el mismo sistema. PyInstaller no hace cross-compile: el .app se genera en macOS, el .exe en Windows. Para ambos, necesitas ambos sistemas (o CI).

④ Antivirus/Gatekeeper. Ejecutables sin firmar generan avisos (macOS: clic derecho → Abrir la primera vez). La firma de código es el paso "distribución seria" (Apple Developer / certificado Windows).

🧪 Tip de entrevista: "¿Cómo distribuyes una app Python de escritorio?" → PyInstaller (o similares: Nuitka, cx_Freeze) empaqueta intérprete+dependencias en un ejecutable por plataforma; los datos del usuario van a su home/AppData; firmar el binario evita los avisos de seguridad.

🔁 4. El flujo de release completo

código OK ──▶ requirements.txt        (pip freeze > requirements.txt)
          ──▶ probar:  python main.py
          ──▶ pyinstaller --windowed --name "MiAgenda" --collect-all customtkinter main.py
          ──▶ probar dist/MiAgenda.app  🔑 EN UNA MÁQUINA SIN PYTHON (o cuenta limpia)
          ──▶ comprimir y compartir (zip / DMG / instalador)

💻 PARTE PRÁCTICA — tu misión

Empaqueta tu To-Do (la versión clase + JSON o SQLite):

  1. Asegúrate de que los datos van a Path.home() / ".mi_todo" (¡no junto al .py!).
  2. En el venv: pip install pyinstaller (ya está en el del curso).
  3. pyinstaller --windowed --name "MiToDo" --collect-all customtkinter main.py
  4. Abre dist/MiToDo.app con doble clic. Agrega tareas, cierra, reabre: ¿persisten?
  5. 📝 Documenta en 06-Errores cualquier tropiezo — los errores de empaquetado son apuntes de oro (frontmatter categoria: "📦 Empaquetado").

🔐 Solución de referencia (ábrela solo DESPUÉS de intentarlo)

Ver solución · la sesión de terminal completa
bash
# 0) siempre desde el venv del proyecto
source .venv/bin/activate

# 1) congelar dependencias (reproducibilidad)
pip freeze > requirements.txt

# 2) probar que la app corre ANTES de empaquetar
python main.py

# 3) empaquetar (macOS; en Windows es el mismo comando)
pyinstaller --windowed --name "MiToDo" --collect-all customtkinter --noconfirm main.py

# 4) queda así:
#    dist/MiToDo.app        ← lo que distribuyes (Windows: dist/MiToDo/MiToDo.exe)
#    build/ y MiToDo.spec   ← artefactos del build
open dist/MiToDo.app       # probarla como la abriría el usuario

# 5) que git ignore los artefactos → añadir a .gitignore:
#    build/
#    dist/
#    *.spec

Si al abrir el .app no pasa nada, corre el binario interno para VER el error: ./dist/MiToDo.app/Contents/MacOS/MiToDo — el traceback aparece en la terminal (el clásico: falta --collect-all customtkinter, o los datos apuntaban junto al .py en vez de Path.home()).

⚠️ El primer build tarda (analiza todo) y dist/ pesa bastante (~50-80 MB): es normal — lleva un Python completo adentro. Agrega build/, dist/ y *.spec a tu .gitignore.


❓ Preguntas y respuestas (autoevaluación)

1. ¿Qué contiene el ejecutable que genera PyInstaller?

El intérprete de Python + tus módulos + las dependencias: por eso corre sin Python instalado.

2. ¿Por qué CustomTkinter necesita --collect-all customtkinter?

Porque trae archivos no-Python (temas .json, imágenes) que el análisis de imports no detecta; sin ellos la app empaquetada no abre.

3. ¿Dónde debe guardar sus datos una app empaquetada y por qué?

En el home del usuario (Path.home() / ".mi_app"): la carpeta del ejecutable es de solo lectura/temporal.

4. ¿Puedes generar el .exe de Windows desde tu Mac?

No: PyInstaller no cross-compila. Cada plataforma empaqueta lo suyo.


📎 Apuntes relacionados

  • Path.home() para los datos → Clase 22
  • La estructura main.py/ui/logica/datos que empaquetas → Clase 29
  • Comandos de venv/pip → 01-Comandos

➡️ Siguiente

Clase 35 · Proyecto final — todo el curso en una app real, escrita por ti.