Proyecto final: Mini librería `contact_book`
Al completar este proyecto serás capaz de:
- estructurar una mini librería reutilizable,
- modelar datos con
dataclass, - validar y normalizar entradas,
- guardar y cargar datos en JSON,
- cubrir el núcleo con pruebas automatizadas.
El paquete se llamará contact_book también en la versión española para mantener una convención realista de nombres de paquete en Python.
El objetivo
Vas a construir una pequeña librería para gestionar contactos. No será una app web ni un script suelto, sino un paquete reutilizable que otro proyecto podría importar.
La idea es integrar en un solo ejercicio lo que ya has practicado por separado:
- estructuras de datos para almacenar contactos y etiquetas,
- funciones para búsquedas, filtros y ordenación,
- POO ligera con
dataclass, - ficheros para persistencia JSON,
- testing para asegurar que el núcleo funciona.
Brief funcional
La librería deberá permitir:
- crear contactos con nombre, email, teléfono y etiquetas,
- normalizar datos de entrada antes de guardarlos,
- validar emails y teléfonos básicos,
- guardar una agenda en JSON y recuperarla,
- buscar por texto,
- filtrar por etiqueta,
- ordenar por nombre.
Estructura objetivo del paquete
1contact_book/
2├── src/
3│ └── contact_book/
4│ ├── __init__.py
5│ ├── models.py
6│ ├── validators.py
7│ ├── storage.py
8│ └── service.py
9├── tests/
10│ ├── test_validators.py
11│ ├── test_storage.py
12│ └── test_service.py
13├── pyproject.toml
14└── README.mdNo intentes escribirlo todo de una vez. Cierra cada hito y añade tests antes de pasar al siguiente.
Hito 1: Modelo y validación
Empieza definiendo el modelo de datos y las funciones de limpieza/validación.
1# src/contact_book/models.py
2from dataclasses import dataclass, field
3
4@dataclass(slots=True)
5class Contact:
6 name: str
7 email: str
8 phone: str = ""
9 tags: list[str] = field(default_factory=list) 1# src/contact_book/validators.py
2import re
3
4EMAIL_RE = re.compile(r"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")
5PHONE_RE = re.compile(r"^[0-9+()\\-\\s]{7,}$")
6
7def normalize_name(name: str) -> str:
8 return " ".join(name.strip().split())
9
10def normalize_tags(tags: list[str]) -> list[str]:
11 return sorted({tag.strip().lower() for tag in tags if tag.strip()})
12
13def validate_email(email: str) -> str:
14 email = email.strip().lower()
15 if not EMAIL_RE.match(email):
16 raise ValueError("Email no válido")
17 return email
18
19def validate_phone(phone: str) -> str:
20 phone = phone.strip()
21 if phone and not PHONE_RE.match(phone):
22 raise ValueError("Teléfono no válido")
23 return phoneHito 2: Persistencia JSON
Cuando el modelo esté claro, crea una capa de almacenamiento simple y predecible.
1# src/contact_book/storage.py
2import json
3from dataclasses import asdict
4from pathlib import Path
5
6from .models import Contact
7
8def save_contacts(path: str | Path, contacts: list[Contact]) -> None:
9 data = [asdict(contact) for contact in contacts]
10 Path(path).write_text(json.dumps(data, indent=2), encoding="utf-8")
11
12def load_contacts(path: str | Path) -> list[Contact]:
13 raw = Path(path).read_text(encoding="utf-8")
14 items = json.loads(raw)
15 return [Contact(**item) for item in items]Hito 3: Operaciones de agenda
Aquí vive el núcleo útil de la librería: añadir, buscar, filtrar y ordenar.
1# src/contact_book/service.py
2from .models import Contact
3from .validators import normalize_name, normalize_tags, validate_email, validate_phone
4
5def build_contact(name: str, email: str, phone: str = "", tags: list[str] | None = None) -> Contact:
6 return Contact(
7 name=normalize_name(name),
8 email=validate_email(email),
9 phone=validate_phone(phone),
10 tags=normalize_tags(tags or []),
11 )
12
13def search_contacts(contacts: list[Contact], text: str) -> list[Contact]:
14 needle = text.strip().lower()
15 return [
16 contact
17 for contact in contacts
18 if needle in contact.name.lower() or needle in contact.email.lower()
19 ]
20
21def filter_by_tag(contacts: list[Contact], tag: str) -> list[Contact]:
22 wanted = tag.strip().lower()
23 return [contact for contact in contacts if wanted in contact.tags]
24
25def sort_contacts(contacts: list[Contact]) -> list[Contact]:
26 return sorted(contacts, key=lambda contact: contact.name.lower())Hito 4: Tests del núcleo
No hace falta probar cada línea, pero sí los comportamientos importantes.
1# tests/test_validators.py
2import pytest
3
4from contact_book.validators import normalize_name, normalize_tags, validate_email
5
6def test_normalize_name_removes_extra_spaces():
7 assert normalize_name(" Ana Pérez ") == "Ana Pérez"
8
9def test_normalize_tags_unique_and_lowercase():
10 assert normalize_tags(["Trabajo", " trabajo ", "", "Familia"]) == ["familia", "trabajo"]
11
12def test_validate_email_rejects_invalid_values():
13 with pytest.raises(ValueError):
14 validate_email("correo-sin-arroba") 1# tests/test_service.py
2from contact_book.service import build_contact, filter_by_tag, search_contacts
3
4def test_build_contact_normalizes_values():
5 contact = build_contact(" Ana Pérez ", "[email protected]", tags=["Trabajo", "trabajo"])
6 assert contact.name == "Ana Pérez"
7 assert contact.email == "[email protected]"
8 assert contact.tags == ["trabajo"]
9
10def test_search_contacts_finds_by_name_and_email():
11 contacts = [
12 build_contact("Ana", "[email protected]"),
13 build_contact("Luis", "[email protected]"),
14 ]
15 assert len(search_contacts(contacts, "ana")) == 1
16
17def test_filter_by_tag_returns_matching_contacts():
18 contacts = [
19 build_contact("Ana", "[email protected]", tags=["trabajo"]),
20 build_contact("Luis", "[email protected]", tags=["familia"]),
21 ]
22 assert len(filter_by_tag(contacts, "trabajo")) == 1Checklist de aceptación
- El paquete usa una estructura tipo
src/. - Existe un
Contactcondataclass. - Los nombres y etiquetas se normalizan antes de guardarse.
- Los emails no válidos lanzan
ValueError. - La librería puede guardar y cargar JSON.
- Hay funciones separadas para búsqueda, filtrado y ordenación.
- El núcleo tiene pruebas automatizadas.
- El proyecto incluye
pyproject.tomlyREADME.md.
Ampliaciones opcionales
Si quieres estirar el proyecto, añade una de estas mejoras:
- soporte de importación/exportación CSV,
- función para actualizar contactos existentes,
- eliminación por email,
- pequeño script de ejemplo en
__main__.py, - más pruebas parametrizadas para validadores.
¡Felicidades!
Has completado el curso de Python. Desde tus primeros print("Hola mundo") hasta una mini librería reutilizable, ya tienes un recorrido coherente entre sintaxis, datos, funciones, ficheros, testing y organización de proyectos.
- Fundamentos para modelar datos y controlar el flujo
- Estructuras de datos para listas, etiquetas y filtros
- Funciones para separar responsabilidades
- POO con
dataclass - Ficheros con persistencia JSON
- Testing para validar el núcleo
- Proyecto con estructura profesional y packaging básico
Próximos pasos
Ahora sí tiene sentido explorar una rama concreta según lo que más te haya gustado:
- FastAPI para APIs limpias y modernas
- Flask si quieres algo pequeño y flexible
- Django si buscas un framework más completo
- pandas para trabajar con tablas y datasets
- NumPy para cálculo numérico
- matplotlib/seaborn para visualización
- requests/httpx para integrarte con APIs
- pathlib y
shutilpara scripts útiles - schedule o tareas del sistema para automatización periódica
- refactoriza tu mini librería,
- añade nuevas pruebas,
- publícala como proyecto personal,
- compara tu solución con librerías reales.