Organización y distribución de proyectos

Objetivos del módulo

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
└── .gitignore

Mó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ón

El archivo __init__.py

Este archivo especial hace dos cosas:

  1. Convierte la carpeta en paquete importable
  2. 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ódulo

Imports 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 procesar

Usan 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.)
setup.py es legado

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-paquete

Uso 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, no x=1+2
  • Nombres: snake_case para funciones y variables, PascalCase para 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 archivos

Antes:

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 defaultdict

Después:

1import os
2import sys
3from collections import defaultdict
4
5from mi_paquete import algo

Linter - 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 = true

Docstrings: 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.db
Nunca subas secretos

Contraseñ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 build

Esto crea en dist/:

  • mi_paquete-1.0.0.tar.gz - Código fuente comprimido
  • mi_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/*
TestPyPI para practicar

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

Ejercicio 1: Reorganiza un script

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()
procesador_datos/
├── src/
│   └── procesador/
│       ├── __init__.py
│       ├── io.py
│       ├── core.py
│       └── cli.py
├── tests/
│   ├── test_io.py
│   └── test_core.py
├── pyproject.toml
└── README.md
1# src/procesador/__init__.py
2from .core import procesar
3from .io import cargar_datos, guardar
4
5__all__ = ["procesar", "cargar_datos", "guardar"]
 1# src/procesador/io.py
 2import json
 3from pathlib import Path
 4
 5def cargar_datos(archivo: str | Path) -> list[dict]:
 6    """Carga datos desde un archivo JSON."""
 7    with open(archivo) as f:
 8        return json.load(f)
 9
10def guardar(datos: list[dict], archivo: str | Path) -> None:
11    """Guarda datos en un archivo JSON."""
12    with open(archivo, 'w') as f:
13        json.dump(datos, f, indent=2)
1# src/procesador/core.py
2def procesar(datos: list[dict]) -> list[dict]:
3    """Filtra registros activos."""
4    return [d for d in datos if d.get('activo', False)]
 1# src/procesador/cli.py
 2from .io import cargar_datos, guardar
 3from .core import procesar
 4
 5def main():
 6    datos = cargar_datos('entrada.json')
 7    procesados = procesar(datos)
 8    guardar(procesados, 'salida.json')
 9    print(f"Procesados {len(procesados)} registros")
10
11if __name__ == '__main__':
12    main()
 1# pyproject.toml
 2[project]
 3name = "procesador-datos"
 4version = "1.0.0"
 5description = "Procesador de datos JSON"
 6requires-python = ">=3.10"
 7
 8[project.scripts]
 9procesar = "procesador.cli:main"
10
11[build-system]
12requires = ["setuptools>=61.0"]
13build-backend = "setuptools.build_meta"
Ejercicio 2: Configura herramientas de calidad

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=valor
 1# pyproject.toml
 2[tool.black]
 3line-length = 88
 4
 5[tool.isort]
 6profile = "black"
 7
 8[tool.ruff]
 9line-length = 88
10select = ["E", "F", "W", "I"]

Después de ejecutar black e isort:

 1import json
 2import os
 3import sys
 4from collections import defaultdict
 5
 6x = 1 + 2
 7
 8
 9def mi_funcion(a, b, c):
10    return a + b + c
11
12
13class MiClase:
14    def __init__(self, valor):
15        self.valor = valor

Quiz

🎮 Quiz: Organización de proyectos

0 / 0
Cargando preguntas...

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.

Siguiente parada

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.


Anterior: Testing Siguiente: Proyecto final