Organización y distribución de proyectos
Al completar este módulo serás capaz de:
- Estructurar proyectos Python de forma profesional
- Crear paquetes reutilizables con módulos organizados
- Escribir documentación efectiva (README, docstrings)
- Aplicar estándares de estilo (PEP8) y usar herramientas de calidad
- Preparar proyectos para compartir o publicar
De script a proyecto: el salto profesional
Cuando empezamos a programar, todo cabe en un solo archivo. Un script calcular.py con 50 líneas resuelve el problema y listo. Pero a medida que el proyecto crece, ese archivo se convierte en un monstruo de 2000 líneas donde nadie encuentra nada.
Es como cuando te mudas a tu primer apartamento. Al principio, todo cabe en una maleta: ropa, libros, algunos trastos. Pero cuando tu vida crece, necesitas organizar: armarios para la ropa, estanterías para los libros, cajones etiquetados para documentos. Un proyecto Python es igual: al principio un script basta, pero cuando creces necesitas carpetas organizadas, módulos separados y documentación clara.
¿Por qué importa la organización?
- Para ti del futuro: Dentro de 6 meses no recordarás qué hace cada función
- Para otros: Si alguien quiere usar o contribuir a tu código, necesita entenderlo
- Para herramientas: Tests, linters y documentación automática esperan cierta estructura
- Para instalación: Un proyecto bien organizado se instala con
pip install
Estructura de un proyecto Python
Piensa en un edificio bien diseñado: cada piso tiene un propósito. Oficinas en uno, apartamentos en otro, parking en el sótano. No mezclas la lavandería con la sala de juntas. Tu proyecto Python debería ser igual: cada carpeta tiene un propósito claro.
Estructura recomendada (moderna)
mi_proyecto/
├── src/
│ └── mi_paquete/
│ ├── __init__.py
│ ├── core.py
│ ├── utils.py
│ └── cli.py
├── tests/
│ ├── __init__.py
│ ├── test_core.py
│ └── test_utils.py
├── docs/
│ └── index.md
├── pyproject.toml
├── README.md
├── LICENSE
└── .gitignore¿Qué es cada cosa?
| Carpeta/Archivo | Propósito |
|---|---|
| src/ | Código fuente del proyecto (opcional pero recomendado) |
| mi_paquete/ | Tu paquete Python con los módulos |
| init.py | Convierte la carpeta en un paquete importable |
| tests/ | Tests automatizados (pytest) |
| docs/ | Documentación del proyecto |
| pyproject.toml | Configuración del proyecto (dependencias, metadatos) |
| README.md | Presentación del proyecto |
| LICENSE | Licencia de uso |
| .gitignore | Archivos que Git debe ignorar |
Estructura simplificada (para proyectos pequeños)
Si tu proyecto es pequeño, puedes omitir src/:
mi_proyecto/
├── mi_paquete/
│ ├── __init__.py
│ └── core.py
├── tests/
│ └── test_core.py
├── pyproject.toml
├── README.md
└── .gitignoreMódulos y paquetes: organizando el código
Piensa en una biblioteca: organiza sus libros en estantes (paquetes), que están en secciones (carpetas), y cada libro es un módulo (archivo .py). El bibliotecario (Python) sabe encontrar cualquier libro si le dices la sección y el estante.
¿Qué es un módulo?
Un módulo es simplemente un archivo .py. Cuando escribes import math, estás importando el módulo math.py.
1# archivo: utils.py (este es un módulo)
2def saludar(nombre):
3 return f"Hola, {nombre}"
4
5PI = 3.14159¿Qué es un paquete?
Un paquete es una carpeta con un archivo __init__.py. Agrupa módulos relacionados.
calculadora/
├── __init__.py
├── basica.py # suma, resta
├── cientifica.py # raíz, potencia
└── financiera.py # interés, amortizaciónEl archivo __init__.py
Este archivo especial hace dos cosas:
- Convierte la carpeta en paquete importable
- Define qué se exporta cuando alguien importa el paquete
1# calculadora/__init__.py
2
3# Importar funciones específicas para acceso directo
4from .basica import suma, resta
5from .cientifica import raiz_cuadrada
6
7# Definir qué se exporta con "from calculadora import *"
8__all__ = ["suma", "resta", "raiz_cuadrada"]
9
10# Versión del paquete
11__version__ = "1.0.0"Ahora puedes hacer:
1from calculadora import suma # Directo, gracias a __init__.py
2from calculadora.financiera import interes_compuesto # Acceso al móduloImports absolutos vs relativos
Especifican la ruta completa desde la raíz del paquete. Recomendados para mayor claridad.
1# Desde cualquier parte del proyecto
2from mi_paquete.utils import formatear
3from mi_paquete.core import procesarUsan puntos para indicar la posición relativa. Útiles dentro del mismo paquete.
1# Desde mi_paquete/core.py
2from .utils import formatear # mismo nivel (.)
3from ..otro_paquete import algo # nivel superior (..)pyproject.toml: el pasaporte del proyecto
Tu pasaporte contiene toda tu información oficial: nombre, nacionalidad, fecha de nacimiento. pyproject.toml es el pasaporte de tu proyecto: nombre, versión, autor, dependencias… Todo lo que necesita saber quien lo encuentre.
Estructura básica
1[project]
2name = "mi-paquete"
3version = "1.0.0"
4description = "Una descripción corta del proyecto"
5readme = "README.md"
6license = {text = "MIT"}
7authors = [
8 {name = "Tu Nombre", email = "[email protected]"}
9]
10requires-python = ">=3.9"
11dependencies = [
12 "requests>=2.28.0",
13 "click>=8.0.0",
14]
15
16[project.optional-dependencies]
17dev = [
18 "pytest>=7.0.0",
19 "black>=23.0.0",
20 "flake8>=6.0.0",
21]
22
23[project.scripts]
24mi-comando = "mi_paquete.cli:main"
25
26[build-system]
27requires = ["setuptools>=61.0"]
28build-backend = "setuptools.build_meta"
29
30[tool.pytest.ini_options]
31testpaths = ["tests"]
32
33[tool.black]
34line-length = 88
35
36[tool.isort]
37profile = "black"Secciones importantes
| Sección | Propósito |
|---|---|
| [project] | Metadatos del proyecto (nombre, versión, autor) |
| dependencies | Paquetes que necesita tu proyecto para funcionar |
| [project.optional-dependencies] | Dependencias opcionales (ej: para desarrollo) |
| [project.scripts] | Comandos de terminal que instala tu paquete |
| [build-system] | Cómo construir el paquete para distribución |
| [tool.*] | Configuración de herramientas (pytest, black, etc.) |
Antes se usaba setup.py para configurar proyectos. Aunque aún funciona, pyproject.toml es el estándar moderno (PEP 517/518). Si ves tutoriales con setup.py, el concepto es el mismo pero la sintaxis difiere.
README: la primera impresión
Cuando llegas a una tienda, el cartel de la entrada te dice qué venden y por qué deberías entrar. El README es ese cartel: lo primero que ven los visitantes de tu proyecto.
Estructura recomendada
1# Nombre del Proyecto
2
3Descripción breve (1-2 frases) de qué hace y para quién.
4
5## Instalación
6
7```bash
8pip install mi-paqueteUso rápido
1from mi_paquete import funcion_principal
2
3resultado = funcion_principal("datos")
4print(resultado)Características
- Característica 1
- Característica 2
- Característica 3
Documentación
Para documentación completa, visita [enlace a docs].
Contribuir
Las contribuciones son bienvenidas. Por favor, lee CONTRIBUTING.md.
Licencia
MIT License - ver LICENSE para detalles.
Tips para un buen README
- Empieza con el “qué”: Descripción clara en las primeras líneas
- Muestra, no cuentes: Un ejemplo de código vale más que párrafos de explicación
- Instalación simple: El lector debe poder instalar en 30 segundos
- Actualízalo: Un README desactualizado es peor que ninguno
Estilo y calidad de código
En un trabajo profesional hay reglas de vestimenta: no porque la ropa afecte tu productividad, sino porque facilita la colaboración y da buena imagen. PEP8 es el “código de vestimenta” de Python: un conjunto de convenciones que todos seguimos para que el código sea legible y consistente.
PEP8: la guía de estilo oficial
PEP8 define cómo debe verse el código Python. Algunas reglas:
- Indentación con 4 espacios (no tabs)
- Líneas de máximo 79-88 caracteres
- Espacios alrededor de operadores:
x = 1 + 2, nox=1+2 - Nombres:
snake_casepara funciones y variables,PascalCasepara clases - Dos líneas en blanco entre funciones de nivel superior
Herramientas automáticas
¿Por qué memorizar reglas cuando las herramientas pueden hacerlo por ti?
Formateador automático - Reformatea tu código automáticamente. “Opinado”: toma decisiones por ti.
1pip install black
2black mi_paquete/ # Formatea todos los archivosAntes:
1x=1+2
2lista=[1,2,3,4,5,6,7,8,9,10,11,12]Después:
1x = 1 + 2
2lista = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]Ordena imports - Agrupa y ordena tus imports automáticamente.
1pip install isort
2isort mi_paquete/Antes:
1import os
2from mi_paquete import algo
3import sys
4from collections import defaultdictDespués:
1import os
2import sys
3from collections import defaultdict
4
5from mi_paquete import algoLinter - Detecta errores de estilo y problemas potenciales.
1pip install flake8
2flake8 mi_paquete/Salida:
mi_paquete/core.py:15:1: E302 expected 2 blank lines, found 1
mi_paquete/utils.py:23:80: E501 line too long (95 > 79 characters)ruff es una alternativa moderna y más rápida:
1pip install ruff
2ruff check mi_paquete/Verificador de tipos - Comprueba que los type hints sean correctos.
1pip install mypy
2mypy mi_paquete/1def sumar(a: int, b: int) -> int:
2 return a + b
3
4sumar("hola", "mundo") # mypy: error!Configuración en pyproject.toml
1[tool.black]
2line-length = 88
3target-version = ['py39']
4
5[tool.isort]
6profile = "black"
7line_length = 88
8
9[tool.ruff]
10line-length = 88
11select = ["E", "F", "W"]
12
13[tool.mypy]
14python_version = "3.9"
15warn_return_any = trueDocstrings: documentando el código
Un buen chef no solo cocina: deja notas explicando sus recetas para que otros puedan reproducirlas. Los docstrings son esas notas en tu código: explican qué hace cada función, qué parámetros recibe y qué devuelve.
Estilos de docstring
1def calcular_descuento(precio: float, porcentaje: float) -> float:
2 """Calcula el precio con descuento aplicado.
3
4 Args:
5 precio: Precio original del producto.
6 porcentaje: Porcentaje de descuento (0-100).
7
8 Returns:
9 Precio final después de aplicar el descuento.
10
11 Raises:
12 ValueError: Si el porcentaje no está entre 0 y 100.
13
14 Example:
15 >>> calcular_descuento(100, 20)
16 80.0
17 """
18 if not 0 <= porcentaje <= 100:
19 raise ValueError("Porcentaje debe estar entre 0 y 100")
20 return precio * (1 - porcentaje / 100) 1def calcular_descuento(precio, porcentaje):
2 """
3 Calcula el precio con descuento aplicado.
4
5 Parameters
6 ----------
7 precio : float
8 Precio original del producto.
9 porcentaje : float
10 Porcentaje de descuento (0-100).
11
12 Returns
13 -------
14 float
15 Precio final después de aplicar el descuento.
16
17 Raises
18 ------
19 ValueError
20 Si el porcentaje no está entre 0 y 100.
21 """
22 if not 0 <= porcentaje <= 100:
23 raise ValueError("Porcentaje debe estar entre 0 y 100")
24 return precio * (1 - porcentaje / 100)Type hints: documentación viva
Los type hints documentan los tipos esperados y permiten verificación automática:
1from typing import Optional, List
2
3def buscar_usuario(
4 nombre: str,
5 edad_minima: Optional[int] = None
6) -> List[dict]:
7 """Busca usuarios por nombre y opcionalmente por edad."""
8 ....gitignore: lo que NO debe subirse
Algunos archivos nunca deben subirse a Git:
# Bytecode de Python
__pycache__/
*.py[cod]
*.pyo
# Entornos virtuales
venv/
.venv/
env/
# Configuración del IDE
.vscode/
.idea/
*.swp
# Archivos de distribución
dist/
build/
*.egg-info/
# Tests y cobertura
.pytest_cache/
.coverage
htmlcov/
# Variables de entorno y secretos
.env
.env.local
*.pem
secrets.json
# Archivos del sistema
.DS_Store
Thumbs.dbContraseñas, API keys, tokens… Si alguna vez subes un secreto a Git, considera que está comprometido aunque lo borres después. Git guarda historial.
Preparando para compartir
Instalar en modo editable
Durante el desarrollo, instala tu paquete en modo “editable”:
1pip install -e .Esto crea un enlace al código fuente. Los cambios que hagas se reflejan inmediatamente sin reinstalar.
Crear distribuciones
Para compartir tu paquete:
1pip install build
2python -m buildEsto crea en dist/:
mi_paquete-1.0.0.tar.gz- Código fuente comprimidomi_paquete-1.0.0-py3-none-any.whl- Wheel (instalación rápida)
Publicar en PyPI
Para que cualquiera pueda hacer pip install tu-paquete:
1pip install twine
2twine upload dist/*Antes de publicar en PyPI real, practica con TestPyPI:
1twine upload --repository testpypi dist/*Licencias comunes
| Licencia | Permite | Requiere |
|---|---|---|
| MIT | Casi todo | Mantener aviso de copyright |
| Apache 2.0 | Casi todo + patentes | Documentar cambios |
| GPL | Casi todo | Código derivado también GPL |
Checklist del proyecto profesional
Antes de compartir tu proyecto, verifica:
| Elemento | ¿Lo tienes? | Prioridad |
|---|---|---|
| README.md con instalación y ejemplo | ☐ | Alta |
| pyproject.toml con dependencias | ☐ | Alta |
| Carpeta tests/ con tests | ☐ | Alta |
| .gitignore configurado | ☐ | Alta |
| Docstrings en funciones públicas | ☐ | Media |
| Type hints | ☐ | Media |
| Linter configurado (black/ruff) | ☐ | Media |
| LICENSE | ☐ | Media |
| CI/CD configurado | ☐ | Baja |
Ejercicios prácticos
Tienes este script monolítico. Reorganízalo en una estructura de proyecto apropiada:
1# todo_en_uno.py
2import json
3
4def cargar_datos(archivo):
5 with open(archivo) as f:
6 return json.load(f)
7
8def procesar(datos):
9 return [d for d in datos if d['activo']]
10
11def guardar(datos, archivo):
12 with open(archivo, 'w') as f:
13 json.dump(datos, f)
14
15def main():
16 datos = cargar_datos('entrada.json')
17 procesados = procesar(datos)
18 guardar(procesados, 'salida.json')
19 print(f"Procesados {len(procesados)} registros")
20
21if __name__ == '__main__':
22 main()Dado este código con problemas de estilo, configura black, isort y ruff en pyproject.toml, y arréglalo:
1import sys,os
2from collections import defaultdict
3import json
4x=1+2
5def miFuncion( a,b,c ):
6 return a+b+c
7class miClase:
8 def __init__(self,valor):self.valor=valorQuiz
🎮 Quiz: Organización de proyectos
Antes del cierre práctico
Ya conoces la parte profesional del ciclo: estructura de carpetas, pyproject.toml, documentación, calidad y distribución. El siguiente paso no es más teoría, sino integrar todo en un proyecto guiado.
En el Proyecto final vas a construir una mini librería reutilizable llamada contact_book, aplicando estructuras de datos, funciones, ficheros, POO ligera y tests.