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:
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)
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.dataclass. Combinadas son
declarativas y autodocumentadas: la clase dice «qué» es y el tipo dice «cómo» se usa.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:
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»):
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:
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))
functools.wraps conserva el nombre y la docstring de la función original,
algo imprescindible para decoradores «limpios».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:
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
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
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.Patrones de diseño aplicados
Los patrones son soluciones probadas a problemas recurrentes. Tres muy útiles en Python:
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())
Testing, logging y proyectos reales
5.1 · pytest: prueba tu código
$ pip install pytest
# 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
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
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
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
✍️ 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,listydonemedianteargparse(p. ej.python gestor.py add "estudiar"). - Tareas en
tareas.json: lista de{"texto", "hecho"}. Repositorio separado (claseTareaRepoconcargar/guardar). - type hints en funciones y
Optionaldonde haga falta. - Dos classes de tarea (base + heredada, p. ej.
TareayTareaUrgente) con herencia. - Un
@contextmanagero función decorada para loggear la duración de la operación. loggingcon nivel INFO (mínimo un info al añadir tarea).- Test
test_gestor.pycon 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")
📝 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 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