🐍 Curso Python · Nivel N4–N5 Avanzado / Experto

Bloque 8: Avanzado y experto

El último escalón. Aquí conviertes lo aprendido en oficio profesional: type hints y tipado, generadores, iteradores y decoradores, context managers, programación funcional, patrones de diseño, tests con pytest, logging y arquitectura de proyectos reales. El reto final une todo en un gestor de tareas por línea de comandos con argparse.

⏱ ≈ 22 horas 📚 5 lecciones ✍️ 15 ejercicios guiados 📝 Examen del bloque + gestor CLI
Lección 1 · Teoría

Type hints y tipado

Los type hints anotan qué tipo espera y devuelve cada cosa. Python no los ejecuta (sigue siendo de tipado dinámico), pero herramientas como mypy y los editores los usan para detectar errores antes de ejecutar:

funciones con anotaciones
def calcular_iva(precio: float, iva: int = 15) -> float:
    return round(precio * (1 + iva / 100), 2)

p: list[str] = ["a", "b"]   # lista de strs

from typing import Optional
def buscar(clave: str, datos: dict[str, int]) -> Optional[int]:
    return datos.get(clave)
💡 Tipos más usados: int, float, str, bool, None, list[str], dict[str, int], tuple[int, str], set[str], Optional[T] (T o None). Verifica con pip install mypy y mypy mi_archivo.py.
⚠️ Un buen proyecto moderno anota también las dataclass. Combinadas son declarativas y autodocumentadas: la clase dice «qué» es y el tipo dice «cómo» se usa.
Lección 2 · Teoría

Generadores, iteradores y decoradores

2.1 · Generadores con yield

Un generador produce valores uno a uno y «recuerda» dónde iba. Ahorra memoria enorme cuando hay millones de datos:

generador de números pares
def pares(limite: int):
    n = 0
    while n < limite:
        yield n
        n += 2

for valor in pares(6):
    print(valor)   # 0, 2, 4

2.2 · Iteradores personalizados

Con __iter__ y __next__ haces que una clase sea iterable («la dices for x in obj»):

contador descendente
class CuentaAtras:
    def __init__(self, inicio: int):
        self.valor = inicio

    def __iter__(self):
        return self

    def __next__(self):
        if self.valor < 0:
            raise StopIteration
        actual = self.valor
        self.valor -= 1
        return actual

for n in CuentaAtras(3):
    print(n)   # 3, 2, 1, 0

2.3 · Decoradores

Un decorador es una función que envuelve a otra para añadirle comportamiento sin tocarla. Es el mecanismo detrás de @app.route, @property y @dataclass:

decorador de medición
import time
from functools import wraps

def cronometra(func):
    @wraps(func)
    def envoltura(*args, **kwargs):
        inicio = time.time()
        resultado = func(*args, **kwargs)
        print(f"{func.__name__}: {time.time() - inicio:.4f}s")
        return resultado
    return envoltura

@cronometra
def calculo_lento(x: int) -> int:
    return sum(range(x))

print(calculo_lento(10_000_000))
💡 Las functools.wraps conserva el nombre y la docstring de la función original, algo imprescindible para decoradores «limpios».
Lección 3 · Teoría

Context managers y programación funcional

3.1 · Crear with ... as

Ya usabas with open(...). Puedes crear tus propios gestores con @contextmanager (de contextlib) aplicando yield:

contextmanager temporizador
from contextlib import contextmanager
import time

@contextmanager
def tiempo_bloque(nombre: str):
    print(f"↗ {nombre}")
    inicio = time.time()
    yield            # aquí dentro va el bloque del with
    print(f"↙ {nombre}: {time.time() - inicio:.2f}s")

with tiempo_bloque("lógica"):
    sum(range(1_000_000))

3.2 · map, filter y reduce funcional

estilo funcional
from functools import reduce

numeros = [1, 2, 3, 4, 5, 6]

doblados = list(map(lambda x: x * 2, numeros))      # [2,4,6,8,10,12]
pares = list(filter(lambda x: x % 2 == 0, numeros))   # [2,4,6]
producto = reduce(lambda a, b: a * b, numeros)             # 720
⚠️ La mayoría de veces una list comprehension es más legible que map/ filter: [x * 2 for x in numeros]. Tiende a lo claro; usa el estilo funcional cuando el pipeline encadene múltiples transformaciones u otras librerías lo pidan.
Lección 4 · Teoría

Patrones de diseño aplicados

Los patrones son soluciones probadas a problemas recurrentes. Tres muy útiles en Python:

Singleton — una sola instancia global de una clase (p. ej. configuración). En Python se logra con una variable módulo o un patrón metaclass.
Factory — una función/clase que decide qué tipo de objeto crear según un parámetro. Desacopla al llamador de las clases concretas.
Repository — separa el acceso a datos (ficheros, BD) del resto de la lógica. Swap fácil: pasar de JSON a SQL sin tocar el resto.
factory simple de animales
class Perro:
    def sonido(self) -> str:
        return "Guau"

class Gato:
    def sonido(self) -> str:
        return "Miau"

def crear_animal(tipo: str):
    if tipo == "perro":
        return Perro()
    if tipo == "gato":
        return Gato()
    raise ValueError("Animal desconocido")

animal = crear_animal("gato")
print(animal.sonido())
💡 No memorices catálogos de patrones. Aprende a reconocer el problema: «siempre la misma instancia», «elegir clase según entrada», «cambiar el almacenamiento sin tocar el resto». El patrón adecuado te caerá encima.
Lección 5 · Teoría

Testing, logging y proyectos reales

5.1 · pytest: prueba tu código

instalar pytest
$ pip install pytest
test de una función
# calculadora.py
def sumar(a: int, b: int) -> int:
    return a + b

# test_calculadora.py
from calculadora import sumar

def test_suma():
    assert sumar(2, 3) == 5

# en la terminal:
$ pytest -q
⚠️ Las funciones de test empiezan con test_, usan assert y viven en archivos test_*.py. pytest las encuentra solo. «Código sin tests» no es código terminado.

5.2 · logging en lugar de print

registro de eventos
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")

logging.debug("detalle innecesario en producción")
logging.info("servicio arrancado")
logging.warning("disco al 90 %")
logging.error("fallo en la BD")

Niveles: DEBUG → INFO → WARNING → ERROR → CRITICAL. Con level= eliges a partir de cuál se muestra.

5.3 · Estructura de un proyecto real

layout recomendado
mi_proyecto/
├── pyproject.toml       # metadatos y dependencias
├── .venv/               # entorno virtual
├── src/
│   └── miniapp/
│       ├── __init__.py
│       ├── modelos.py
│       ├── servicios.py
│       └── repositorio.py
├── tests/
│   ├── test_modelos.py
│   └── test_servicios.py
└── README.md
🎉 Fin del curso. Type hints, generadores, decoradores, tests, logs y patrones: el kit completo de nivel experto. A partir de aquí, los problemas concretos se resuelven eligiendo entre lo que ya dominas.
Manos a la obra

✍️ Los 15 ejercicios del bloque

Crea la carpeta python-b8/ y un archivo .py por ejercicio. Los últimos son un mini proyecto con tests.

Sección A · Tipos, generadores y decoradores

1 Suma tipada

Define suma_lista(valores: list[float]) -> float con type hints explícitos y pruébala. Ejecuta mypy opcional si lo tienes.

Ver solución
def suma_lista(valores: list[float]) -> float:
    return sum(valores)

print(suma_lista([1.5, 2.5]))   # 4.0

2 Optional como retorno

Define buscar(nombre: str, agenda: dict[str, str]) -> Optional[str] que devuelva el teléfono o None. Importa Optional de typing.

Ver solución
from typing import Optional

def buscar(nombre: str, agenda: dict[str, str]) -> Optional[str]:
    return agenda.get(nombre)

agenda = {"Ana": "6000001"}
print(buscar("Ana", agenda))       # 6000001
print(buscar("Luis", agenda))     # None

3 Generador de fibonacci

Crea un generador fibonacci(limite) con yield que emita los primeros limite números de la serie (0, 1, 1, 2, 3, 5…).

Ver solución
def fibonacci(limite: int):
    a, b = 0, 1
    for _ in range(limite):
        yield a
        a, b = b, a + b

print(list(fibonacci(7)))   # [0,1,1,2,3,5,8]

4 Iterador de letras

Clase Abc con __iter__/__next__ que recorra las letras de «python» y pare con StopIteration al final. Recórrela con un for.

Ver solución
class Abc:
    def __init__(self, texto: str):
        self.texto = texto
        self.pos = 0

    def __iter__(self):
        return self

    def __next__(self) -> str:
        if self.pos >= len(self.texto):
            raise StopIteration
        letra = self.texto[self.pos]
        self.pos += 1
        return letra

for letra in Abc("python"):
    print(letra, end=" ")   # p y t h o n

5 Decorador arriba

Crea el decorador mayusculas(func) que devuelva el resultado del decorado convertido a mayúsculas. Aplícalo a saludo(nombre).

Ver solución
from functools import wraps

def mayusculas(func):
    @wraps(func)
    def envoltura(*args, **kwargs):
        resultado = func(*args, **kwargs)
        return resultado.upper()
    return envoltura

@mayusculas
def saludo(nombre: str) -> str:
    return f"hola, {nombre}"

print(saludo("ana"))   # HOLA, ANA

Sección B · Funcional y context managers

6 Cuadrados funcional

Con map y una lambda, devuelve los cuadrados de [1, 2, 3, 4]. Después haz lo mismo con una list comprehension y compáralos.

Ver solución
numeros = [1, 2, 3, 4]
print(list(map(lambda x: x * x, numeros)))   # [1,4,9,16]
print([x * x for x in numeros])            # igual

7 Filtrar positivos

Con filter, conserva solo los positivos de [-3, 5, -1, 8, 0]. Pruébalo.

Ver solución
numeros = [-3, 5, -1, 8, 0]
print(list(filter(lambda x: x > 0, numeros)))   # [5, 8]

8 Producto con reduce

Con functools.reduce multiplica todos los elementos de [1, 2, 3, 4, 5]. Debe dar 120.

Ver solución
from functools import reduce

print(reduce(lambda a, b: a * b, [1, 2, 3, 4, 5]))   # 120

9 Context manager propio

Con @contextmanager crea abrir_lectura(ruta) que imprima «Abriendo…» antes del yield y «Cerrando…» después. Úsalo con with para leer un archivo.

Ver solución
from contextlib import contextmanager

@contextmanager
def abrir_lectura(ruta: str):
    print("Abriendo…")
    f = open(ruta, encoding="utf-8")
    try:
        yield f
    finally:
        f.close()
        print("Cerrando…")

with abrir_lectura("mensaje.txt") as f:
    print(f.read())

10 Factory de figuras

Clases Cuadrado y Circulo con método area(), y función crear_figura(tipo, lado_o_radio) que devuelva la adecuada. Puebla una lista y muestra sus áreas con polimorfismo.

Ver solución
import math

class Cuadrado:
    def __init__(self, lado: float):
        self.lado = lado
    def area(self) -> float:
        return self.lado ** 2

class Circulo:
    def __init__(self, radio: float):
        self.radio = radio
    def area(self) -> float:
        return math.pi * self.radio ** 2

def crear_figura(tipo: str, medida: float):
    if tipo == "cuadrado":
        return Cuadrado(medida)
    if tipo == "circulo":
        return Circulo(medida)
    raise ValueError("Figura desconocida")

figuras = [crear_figura("cuadrado", 3), crear_figura("circulo", 2)]
print([f.area() for f in figuras])

11 Repository de configuración

Crea RepositorioConfig con cargar(ruta) y guardar(ruta, datos) que manejen un dict en JSON. Luego úsalo para guardar y leer una config.

Ver solución
import json

class RepositorioConfig:
    def cargar(self, ruta: str):
        with open(ruta, encoding="utf-8") as f:
            return json.load(f)

    def guardar(self, ruta: str, datos: dict):
        with open(ruta, "w", encoding="utf-8") as f:
            json.dump(datos, f, ensure_ascii=False, indent=2)

repo = RepositorioConfig()
repo.guardar("cfg.json", {"tema": "oscuro"})
print(repo.cargar("cfg.json"))

Sección C · Testing y logging

12 Primer test con pytest

Crea funciones.py con es_par(n) y test_funciones.py con dos funciones de test (par y no par). Ejecuta pytest -q.

Ver solución
# funciones.py
def es_par(n: int) -> bool:
    return n % 2 == 0

# test_funciones.py
from funciones import es_par

def test_par():
    assert es_par(4) is True

def test_impar():
    assert es_par(7) is False

13 Test con pytest.raises

Define dividir(a, b) que lance ZeroDivisionError. En el test usa pytest.raises(ZeroDivisionError).

Ver solución
# division.py
def dividir(a: float, b: float) -> float:
    return a / b

# test_division.py
import pytest
from division import dividir

def test_correcta():
    assert dividir(10, 2) == 5

def test_zero():
    with pytest.raises(ZeroDivisionError):
        dividir(10, 0)

14 Logging informativo

Configura logging con formato fecha+ nivel+mensaje en nivel INFO y escribe un debug, un info y un error.

Ver solución
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")

logging.debug("no se ve (por debajo de INFO)")
logging.info("servicio arrancado")
logging.error("fallo en la BD")

15 🏆 Proyecto final: «gestor de tareas CLI»

Crea gestor.py: una herramienta de línea de comandos con argparse que gestione tareas guardadas en JSON. Requisitos integradores:

  • Commandos add, list y done mediante argparse (p. ej. python gestor.py add "estudiar").
  • Tareas en tareas.json: lista de {"texto", "hecho"}. Repositorio separado (clase TareaRepo con cargar/guardar).
  • type hints en funciones y Optional donde haga falta.
  • Dos classes de tarea (base + heredada, p. ej. Tarea y TareaUrgente) con herencia.
  • Un @contextmanager o función decorada para loggear la duración de la operación.
  • logging con nivel INFO (mínimo un info al añadir tarea).
  • Test test_gestor.py con pytest (al menos 2 test del repositorio o de la función de filtrado).
  • Docstrings y nombres snake_case.
Ver una solución posible
import argparse
import json
import logging
from pathlib import Path

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")

class Tarea:
    def __init__(self, texto: str, hecho: bool = False):
        self.texto = texto
        self.hecho = hecho

class TareaUrgente(Tarea):
    def __init__(self, texto: str):
        super().__init__(texto, hecho=False)
        self.urgente = True

class TareaRepo:
    """Persistencia de tareas en un fichero JSON."""
    def __init__(self, ruta: str = "tareas.json"):
        self.ruta = Path(ruta)

    def cargar(self) -> list[dict]:
        if not self.ruta.exists():
            return []
        with open(self.ruta, encoding="utf-8") as f:
            return json.load(f)

    def guardar(self, tareas: list[dict]):
        with open(self.ruta, "w", encoding="utf-8") as f:
            json.dump(tareas, f, ensure_ascii=False, indent=2)

repo = TareaRepo()

parser = argparse.ArgumentParser(description="Gestor de tareas CLI")
parser.add_argument("accion", choices=["add", "list", "done"])
parser.add_argument("texto", nargs="?", default="")
args = parser.parse_args()

tareas = repo.cargar()

if args.accion == "add":
    tareas.append(TareaUrgente(args.texto).__dict__) if args.texto.startswith("!") else tareas.append(Tarea(args.texto).__dict__)
    repo.guardar(tareas)
    logging.info("tarea añadida: %s", args.texto)
elif args.accion == "list":
    for i, t in enumerate(tareas, start=1):
        print(f"[{i}] {'✔' if t['hecho'] else '·'} {t['texto']}")
elif args.accion == "done":
    tareas[int(args.texto) - 1]["hecho"] = True
    repo.guardar(tareas)
    logging.info("tarea completada")
Evaluación final del curso

📝 Examen del Bloque 8

Dos partes: un test teórico autoevaluable (mínimo 70 %) y un reto práctico de autocorrección con laboratorio.

Parte A · Test teórico (aprueba con 11/15)

1 · ¿Qué hacen los type hints?
2 · ¿Qué herramienta verifica los type hints?
3 · ¿Qué palabra convierte una función en generador?
4 · ¿Qué excepción señala el final de una iteración?
5 · ¿Para qué sirve un decorador?
6 · ¿Qué conserva wraps(func) en un decorador?
7 · ¿Qué hace map(lambda x: x*2, lista)?
8 · ¿Cuál es la diferencia de filter respecto a la condición?
9 · ¿Qué hay que importar para usar @contextmanager?
10 · ¿Cuál es la ventaja del patrón Repository?
11 · ¿Qué comando ejecuta los tests con pytest?
12 · ¿Con qué deben empezar las funciones de test?
13 · ¿Qué nivel de logging es más grave que WARNING?
14 · ¿Qué biblioteca crea la interfaz de comandos del reto?
15 · ¿Cuál es la estructura de proyecto recomendada?

Parte B · Reto «kit experto»

Crea experto.py donde juntes las técnicas del bloque. Valida tú mismo/a estos criterios. Mínimo para aprobar: 6 de 8 casillas. Si sacas 8/8… ¡eres oficialmente Pythonista! 🎓

Cumplidos: 0 / 8

🏆 ¡Enhorabuena! Has completado los 8 bloques del curso de Python de TécnicoWeb: de «hola mundo» a proyectos reales con tipo, tests, APIs, datos y patrones. Desde aquí, cada tema nuevo (Django, FastAPI, ML, automatización) será un pasito en terreno conocido. Keep calm and code Python. 🐍🔥