1.18 · Proyecto final de la unidad¶
Objetivos. Al terminar este proyecto podrás: convertir un problema pequeño en un programa Python mantenible; separar datos, lógica e interfaz; elegir estructuras y módulos; persistir resultados; manejar fallos; y justificar la calidad de una solución con pruebas.
Evidencia de logro. Entregarás una aplicación ejecutable, tipada y testeada que procese una entrada reproducible, produzca una salida útil y documente sus decisiones. La aplicación será el cierre de la progresión script → datos → funciones → módulos → objetos → proyecto verificado.
Antes de empezar
Es el proyecto final de la unidad: usa todo lo de los capítulos anteriores.
Antes de empezar, comprueba que puedes ejecutar la práctica de
1.17 · Type hints y testing, porque el
entregable se evalúa con pytest.
Contexto y motivación¶
Un proyecto final no consiste en juntar todas las APIs aprendidas. Consiste en definir un problema acotado y demostrar que sabes construir una solución que otra persona puede ejecutar, entender y comprobar.
Puedes trabajar con un asistente de notas, un analizador de texto, un monitor de tendencias o un generador de fractales. En todos los casos se evalúa el mismo núcleo ingenieril; el tema no debe ocultar la ausencia de un contrato claro.
Prerrequisitos¶
1.11 · Excepciones y logging, 1.12 · Módulos y paquetes, 1.13 · POO, 1.14 · Biblioteca estándar, 1.15 · Librerías externas, 1.16 · Asincronía y 1.17 · Type hints y testing.
La asincronía es opcional en el núcleo del proyecto; tipos, errores y pruebas son obligatorios.
1. Elegir un problema acotado¶
Escoge una propuesta o formula otra con el mismo tamaño:
| Opción | Entrada | Salida mínima | Ampliación |
|---|---|---|---|
| Asistente de notas | JSON local con notas | búsqueda, filtrado y resumen | consulta HTTP simulada |
| Analizador de sentimientos | CSV o lista de reseñas | etiqueta y recuento por etiqueta | comparación con un modelo externo |
| Monitor de tendencias | CSV de publicaciones | palabras más frecuentes y evolución | consulta asíncrona simulada |
| Chatbot de reglas | mensajes de texto | respuesta y estado de conversación | persistencia de historial |
| Fractales Mandelbrot | parámetros numéricos | matriz o imagen reproducible | explorador interactivo |
Un analizador basado en palabras positivas y negativas es un baseline de reglas, no un modelo de aprendizaje automático. Describe con precisión qué hace tu programa y no presentes una heurística como inteligencia general.
1.1 Contrato mínimo¶
Antes de programar, escribe una ficha de una página:
- Usuario y problema: quién lo usa y qué decisión o tarea facilita.
- Entradas: formato, tipos, campos obligatorios y valores inválidos.
- Salida: estructura, significado y ejemplo.
- Restricciones: sin red, sin claves, tiempo y tamaño de datos.
- Criterio de éxito: tres casos comprobables.
- Fuera de alcance: funcionalidades que no intentarás en esta entrega.
Si no puedes escribir un caso de entrada y una salida esperada, el problema aún no está suficientemente definido.
2. Estructura recomendada¶
Parte de esta estructura y cambia los nombres según tu dominio:
proyecto_final/
├── pyproject.toml
├── README.md
├── datos/
│ └── ejemplo.json
├── asistente/
│ ├── __init__.py
│ ├── modelo.py
│ ├── servicio.py
│ └── cli.py
└── tests/
├── test_modelo.py
└── test_servicio.py
Responsabilidades:
| Módulo | Responsabilidad |
|---|---|
modelo.py |
Clases, Enum e invariantes del dominio |
servicio.py |
Funciones que transforman datos y coordinan operaciones |
cli.py |
Entrada/salida de terminal y códigos de respuesta |
tests/ |
Contratos automatizados |
datos/ |
Casos de ejemplo pequeños y públicos |
La interfaz no debería volver a calcular la lógica del servicio, y el modelo no
debería llamar a input() ni escribir directamente en la terminal.
Recuerda añadir a pyproject.toml la configuración de 1.17 para que las pruebas
de tests/ puedan importar tu paquete:
3. Construir una línea de base¶
Empieza con el caso más pequeño que funcione. Por ejemplo, un clasificador local puede definir primero un contrato explícito:
from collections import Counter
PALABRAS_POSITIVAS = {"útil", "bueno", "claro"}
PALABRAS_NEGATIVAS = {"difícil", "malo", "lento"}
def clasificar(texto: str) -> str:
if not texto.strip():
raise ValueError("El texto no puede estar vacío")
palabras = set(texto.lower().split())
positivas = len(palabras & PALABRAS_POSITIVAS)
negativas = len(palabras & PALABRAS_NEGATIVAS)
if positivas > negativas:
return "positivo"
if negativas > positivas:
return "negativo"
return "neutro"
def resumir(etiquetas: list[str]) -> dict[str, int]:
return dict(Counter(etiquetas))
etiquetas = [clasificar(texto) for texto in [
"el resultado es útil y claro",
"el proceso es lento",
"sin opinión",
]]
print(etiquetas)
print(resumir(etiquetas))
Salida esperada:
La línea de base permite comprobar el flujo antes de añadir clases, persistencia o una librería externa. En tu proyecto sustituye este ejemplo por una operación propia y conserva una salida pequeña que sirva como referencia.
4. Integrar clases y estados¶
Cuando el dominio tiene identidad y estados, usa el modelo de 1.13:
from enum import Enum
class Estado(Enum):
NUEVO = "nuevo"
PROCESADO = "procesado"
ERROR = "error"
class Resultado:
def __init__(self, entrada: str, etiqueta: str):
self.entrada = entrada
self.etiqueta = etiqueta
self.estado = Estado.PROCESADO
def como_dict(self) -> dict[str, str]:
return {
"entrada": self.entrada,
"etiqueta": self.etiqueta,
"estado": self.estado.value,
}
resultado = Resultado("mensaje de ejemplo", "neutro")
print(resultado.como_dict())
Salida esperada:
El ejemplo no exige crear una jerarquía de clases. Si tu dominio no necesita herencia, composición y funciones pueden ser una decisión mejor. La calidad se mide por la adecuación al problema, no por el número de decoradores o clases.
5. Persistencia y errores¶
Guarda resultados en JSON con Path, valida las entradas y registra operaciones
importantes. Reutiliza las funciones de 1.10 y el manejo de 1.11; no copies
bloques en cada módulo.
Una ruta de datos de ejemplo debe poder reconstruirse desde cero. No dependas de
un fichero creado manualmente en una ejecución anterior: incluye el caso mínimo
en datos/ejemplo.json o créalo explícitamente durante la práctica.
Comprueba al menos:
- fichero ausente;
- JSON inválido o campo obligatorio ausente;
- entrada válida vacía, si el dominio la permite;
- escritura y lectura de la salida.
No trates un JSON corrupto como si fuera una lista vacía sin registrarlo: podrías perder información y no saber que la aplicación está trabajando con datos incompletos.
6. Tipos y pruebas¶
Cada función pública debe tener anotaciones útiles. Prueba comportamientos, no la forma exacta del código:
import pytest
from asistente.servicio import clasificar
@pytest.mark.parametrize(
("texto", "esperada"),
[
("útil y claro", "positivo"),
("muy lento", "negativo"),
("sin coincidencias", "neutro"),
],
)
def test_clasificar(texto: str, esperada: str) -> None:
assert clasificar(texto) == esperada
def test_entrada_no_valida():
with pytest.raises(ValueError, match="no puede estar vacío"):
clasificar(" ")
El error esperado debe corresponder a una regla del contrato: aquí no se admite un texto vacío. Si tu proyecto acepta entradas vacías, sustituye el caso por otra precondición que sí exista y documenta la decisión. No entregues una prueba vacía solo para aumentar el número de tests.
Ejecuta:
Salida esperada:
Conserva el número real de pruebas y revisa que una prueba que falla haga fallar la entrega. Una suite que siempre pasa porque no contiene aserciones no aporta evidencia.
7. Plan de trabajo en cuatro horas¶
| Fase | Tiempo orientativo | Evidencia |
|---|---|---|
| Contrato y diseño | 30 min | ficha de problema, entradas y salidas |
| Línea de base | 45 min | caso mínimo ejecutado |
| Organización | 60 min | módulos y responsabilidades |
| Robustez | 45 min | errores, persistencia y logging |
| Pruebas y documentación | 45 min | suite, README y decisiones |
| Revisión final | 15 min | ejecución desde cero y lista de límites |
El proyecto puede continuar con trabajo autónomo. No añadas una funcionalidad nueva si la anterior no tiene un caso de aceptación.
8. README y reproducibilidad¶
El README.md debe incluir:
- problema y fuera de alcance;
- instalación (
uv sync --lockedsi el proyecto incluyeuv.lock; en otro caso,uv syncpara resolver las dependencias); - ejecución (
uv run python -m asistente.clio equivalente); - pruebas (
uv run pytest); - ejemplo de entrada y salida;
- estructura de módulos;
- decisiones, limitaciones y posibles mejoras.
Haz una prueba limpia: desde una copia o después de borrar los resultados generados, ejecuta la instalación, la aplicación y los tests. Esa comprobación revela dependencias ocultas y rutas escritas a mano.
Entrega y criterios de aceptación¶
Entrega 14_proyecto_final.py como cuaderno marimo de presentación o script de
entrada, el proyecto completo y la carpeta tests/. El código debe:
- ejecutarse con
uv run; - separar interfaz, lógica y modelo;
- usar al menos una función tipada y una estructura de datos adecuada;
- persistir o leer datos de forma reproducible;
- manejar un fallo previsible y registrarlo;
- tener pruebas del caso normal, un límite y un error;
- incluir una conclusión que interprete el resultado y sus límites.
Rúbrica orientativa¶
| Criterio | Peso | Evidencia |
|---|---|---|
| Contrato y utilidad | 20 % | problema acotado y salidas interpretadas |
| Diseño | 25 % | módulos, funciones, clases y estructuras justificadas |
| Robustez | 20 % | validación, excepciones, persistencia y logging |
| Verificación | 25 % | tests deterministas y límites relevantes |
| Reproducibilidad | 10 % | README, uv.lock y ejecución desde cero |
Una ampliación asíncrona o una librería externa no compensa un núcleo que no se puede ejecutar o comprobar. Documenta cualquier dependencia opcional y ofrece siempre una ruta local para evaluar el contrato básico.
Errores frecuentes¶
- Elegir un proyecto demasiado grande y entregar solo una demo incompleta.
- Añadir una API externa antes de comprobar el flujo con datos locales.
- Llamar «IA» a una regla sin explicar su alcance y sus limitaciones.
- Mezclar
input, persistencia, lógica y presentación en una sola función. - Usar rutas absolutas o ficheros que no se incluyen en la entrega.
- Capturar errores y continuar con datos vacíos sin dejar registro.
- Crear tests que dependen de la hora, la red o el estado de otra prueba.
- Confundir cantidad de código con evidencia de aprendizaje.
Checklist antes de entregar¶
- El contrato tiene una entrada y una salida esperada.
- El caso mínimo funciona desde una carpeta limpia.
- Las funciones públicas tienen anotaciones útiles.
- El programa distingue un fallo recuperable de un bug inesperado.
- Hay logging suficiente para investigar una operación fallida.
-
uv run pytest -qpasa y las pruebas contienen aserciones. - El README explica instalar, ejecutar, probar y las limitaciones.
-
uv.lockestá actualizado y versionado;.envy cualquier clave no lo están.
Resumen y referencia rápida¶
Este capítulo no introduce APIs nuevas: reúne los patrones de la unidad que el proyecto debe demostrar.
| Necesitas | Capítulo | Patrón |
|---|---|---|
| Validar y rechazar entradas | 1.11 | raise ValueError("...") |
| Registrar operaciones | 1.11 | logger.info(...) / logger.exception(...) |
| Separar responsabilidades | 1.12 | módulos con una responsabilidad cada uno |
| Punto de entrada | 1.12 | if __name__ == "__main__": |
| Estado del dominio | 1.13 | clase con @property validada y Enum |
| Fechas, CSV, conteos | 1.14 | datetime, csv.DictReader, Counter |
| Configuración externa | 1.15 | os.environ.get o .env sin secretos |
| Contrato de funciones | 1.17 | anotaciones de tipo en parámetros y retorno |
| Verificación | 1.17 | pytest.raises, parametrize, tmp_path |
Cierre de la unidad¶
Has pasado de ejecutar instrucciones sueltas a construir un programa con responsabilidades, contratos y evidencia. En la UD2 reutilizarás esta disciplina para crear una API: las funciones se convertirán en rutas, la validación será un contrato HTTP y los tests comprobarán el comportamiento de la aplicación.