El módulo ha ido dejando avisos por el camino: print("Aviso: no existe catalogo.json..."), print("Fila ignorada..."). Funcionaban en clase, pero imagina la escena real: Ana cierra la tienda un sábado, apaga el ordenador y el lunes el cierre de caja no cuadra. ¿Qué pasó el sábado? Los print() murieron con la terminal; no queda ni rastro. Los avisos por pantalla sirven para quien está mirando; para diagnosticar lo que pasó cuando nadie miraba hace falta un registro persistente: el módulo logging de la biblioteca estándar. En esta última lección del módulo aprenderás sus niveles, su configuración básica hacia datos/papyrus.log, el patrón del logger por módulo y la joya logger.exception(), que guarda el traceback completo en el archivo. Cerraremos con el decálogo de buenas prácticas de todo el módulo 7 y con un caja.py que junta las tres piezas: try/except, excepciones propias y logging.
Contenido
- Por qué
print()no sirve para diagnosticar - Los cinco niveles: de
DEBUGaCRITICAL logging.basicConfig: formato y salida a archivo- Un logger por módulo:
getLogger(__name__) logger.exception(): el traceback, al log- Qué registrar y qué no
- Decálogo del módulo 7 y el
caja.pyrobusto
Por qué print() no sirve para diagnosticar
print() y logging responden a preguntas distintas: print() habla con el usuario que está delante; el log habla con el desarrollador que llegará después. En cuanto el programa vive más de una sesión, print() como herramienta de diagnóstico falla por todos lados:
| Necesidad en producción | print() |
logging |
|---|---|---|
| Que quede rastro al cerrar la terminal | No: se esfuma | Sí: archivo persistente |
| Saber cuándo pasó cada cosa | Habría que concatenar la fecha a mano en cada print | Timestamp automático en el formato |
| Distinguir rutina de emergencia | Todo se imprime igual | Niveles: INFO vs ERROR |
| Subir/bajar el detalle sin tocar código | Añadir/borrar prints por todo el fichero | Cambiar level= en un solo sitio |
| Guardar el traceback de una excepción manejada | Copiarlo a mano (nadie lo hace) | logger.exception() lo hace solo |
| Saber qué módulo habló | Anónimo | El nombre del logger va en cada línea |
Ojo al matiz: print() no queda prohibido. El menú de la caja, el "Venta realizada: 20.70 €", los mensajes a Júlia y Omar — eso es interfaz de usuario y seguirá siendo print() (hasta que el módulo 10 lo convierta en web). Lo que emigra al log es la telemetría: avisos, errores, migas de pan para el diagnóstico.
Los cinco niveles: de DEBUG a CRITICAL
Cada mensaje de log lleva un nivel de gravedad. Son cinco, ordenados, y el logger tiene un umbral: solo registra lo que iguala o supera su nivel configurado.
| Nivel | Cuándo usarlo (criterio Papyrus) | Ejemplo |
|---|---|---|
DEBUG |
Detalle fino para desarrollar; en producción suele ir apagado | "clave normalizada: 'la odisea'" |
INFO |
La vida normal de la tienda: hitos que confirman que todo va bien | "Catálogo cargado: 4 libros" / "Venta: Hamlet x2, 20.70 €" |
WARNING |
Algo raro pero el programa continúa; alguien debería mirarlo | "Fila 7 de ventas.csv ilegible, ignorada" |
ERROR |
Una operación ha fallado; el programa sobrevive pero esa acción no se hizo | "No se pudo guardar el catálogo" |
CRITICAL |
El programa no puede continuar | "catalogo.json corrupto y sin copias en copias/" |
La prueba del algodón para elegir nivel: ¿qué debería hacer quien lo lea? Nada, solo contexto → DEBUG/INFO. Echar un vistazo esta semana → WARNING. Actuar hoy → ERROR. Dejar lo que esté haciendo y correr → CRITICAL. Con el umbral en INFO (típico de producción), los DEBUG se descartan sin coste; bajas a DEBUG cuando persigues un fallo, y el mismo código se vuelve locuaz sin añadir un solo print.
logging.basicConfig: formato y salida a archivo
Una sola llamada, al principio del programa principal y solo una vez, configura el sistema entero:
# caja.py — al arrancar, antes de cualquier otra cosa
import logging
from pathlib import Path
BASE = Path(__file__).parent # el ancla del módulo 6
(BASE / "datos").mkdir(exist_ok=True)
logging.basicConfig(
filename=BASE / "datos" / "papyrus.log",
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
encoding="utf-8",
)Pieza a pieza:
filename: los mensajes van adatos/papyrus.logen vez de a pantalla. Se abre en modo append (a, del módulo 6): cada arranque suma, no machaca — el historial es justamente el valor.level=logging.INFO: el umbral.DEBUGse descarta; deINFOpara arriba, se escribe.format: la plantilla de cada línea.%(asctime)ses la fecha y hora,%(levelname)-8sel nivel alineado a 8 caracteres,%(name)sel logger que habla,%(message)stu texto. (Es la sintaxis clásica de%, anterior a las f-strings;loggingla conserva por historia y rendimiento.)encoding="utf-8": el innegociable del módulo 6 también rige aquí — en el log habrá títulos con tildes.
El resultado, tal como se ve dentro de papyrus.log:
2026-07-13 09:00:12,451 INFO almacen: Catálogo cargado: 4 libros
2026-07-13 09:14:03,102 INFO almacen: Venta: Hamlet x2, 20.70 €
2026-07-13 09:31:44,876 WARNING caja: Fila ignorada en ventas.csv: {'fecha': '2026-07-12', 'titulo': 'Fausto', 'importe': 'gratis'}Fecha, gravedad, módulo, mensaje con el dato culpable: el lunes por la mañana, Ana (o tú) reconstruye el sábado línea a línea.
Un logger por módulo: getLogger(__name__)
No se llama a logging.info(...) a pelo por todo el código. El patrón profesional crea un logger con nombre en la cabecera de cada módulo:
# almacen.py
import logging
logger = logging.getLogger(__name__) # nombre: "almacen"
def cargar_catalogo():
...
logger.info("Catálogo cargado: %d libros", len(catalogo))__name__es la variable que el módulo 3 nos presentó (la delif __name__ == "__main__":): vale"almacen"cuando el módulo se importa. Así, cada línea del log declara su origen — la columna%(name)sdel formato — sin esfuerzo.- División de poderes: los módulos (
almacen.py,errores.py...) emiten con su logger; solo el programa principal configura conbasicConfig. Una biblioteca que configurara el logging pisaría la configuración de quien la importa. - El estilo
%dcon argumentos separados (logger.info("...: %d libros", len(catalogo))) deja queloggingcomponga el texto solo si el mensaje pasa el umbral. Con f-strings también funciona y es aceptable mientras aprendes; el estilo diferido es el idiomático.
logger.exception(): el traceback, al log
La joya de la corona. Dentro de un except, logger.exception(...) registra el mensaje a nivel ERROR y añade el traceback completo de la excepción en curso — el informe forense de 07-01, archivado automáticamente:
def guardar_catalogo(catalogo):
ruta = BASE / "datos" / "catalogo.json"
try:
with open(ruta, "w", encoding="utf-8") as f:
json.dump([asdict(l) for l in catalogo.values()], f,
ensure_ascii=False, indent=2)
except OSError:
logger.exception("No se pudo guardar el catálogo en %s", ruta)
raise # registrar NO es manejar: que decida quien llamóEn papyrus.log aparece la línea ERROR seguida del Traceback (most recent call last): ... íntegro, con fichero, línea y causa. Tres reglas de uso:
- Solo dentro de un
except— es donde existe "la excepción en curso" que adjuntar. - Combina perfectamente con el
raisea secas de 07-03: registrar en la frontera y relanzar. El log guarda la evidencia; la excepción sigue su viaje hacia quien pueda decidir. Registrar no es manejar. - Sustituye al antipatrón definitivo:
except OSError: passera silencio culpable (07-02);except OSError: logger.exception(...)seguido de una decisión consciente (relanzar, valor por defecto, abortar) es ingeniería.
Qué registrar y qué no
Un log útil cuenta la historia operativa del programa. Registra: arranque y parada (con versión y configuración cargada), hitos de negocio a INFO (ventas, altas, reposiciones, cierres de caja), toda anomalía a WARNING+ con el dato culpable ({fila!r}, la ruta, el título), y el traceback vía logger.exception() en cada except fronterizo.
Y lo que no debe entrar, porque el log es un archivo de texto plano que se copia, se comparte y acaba en correos de soporte:
- Datos personales y sensibles: nada de nombres completos de clientes asociados a sus compras, direcciones, teléfonos, datos bancarios o de tarjetas.
"Venta: Hamlet x1, socio MARTA-002"vale (un código interno opaco);"Marta García, tarjeta 4779..."jamás. La legislación de protección de datos (en Europa, el RGPD) convierte esto en obligación legal, no solo en cortesía — y un log no tiene el control de acceso de una base de datos. - Secretos: contraseñas, tokens, claves de API. Ni siquiera "para depurar un momento": los logs se archivan y los secretos ahí escritos quedan comprometidos.
- Ruido: registrar cada iteración de un bucle a
INFOentierra la señal. Para eso existeDEBUG, que en producción va apagado.
Decálogo del módulo 7 y el caja.py robusto
Las diez reglas que resumen todo el módulo, cada una con su lección de origen:
- Lee el traceback de abajo arriba: tipo y mensaje primero, cadena de llamadas después (07-01).
- EAFP frente al mundo exterior: intenta y maneja;
Path.exists()era un paliativo con condición de carrera (07-01). - Captura tipos concretos: jamás
except:pelado;Exceptionsolo en la última frontera, y registrando (07-02). trymínimo: solo la línea que puede fallar;elsepara el camino feliz,finallypara la limpieza incondicional (07-02).- No silencies: un error atrapado se cuenta, se registra o se relanza; nunca
passa secas (07-02, 07-05). - Falla pronto: guard clauses en la puerta, mutación del estado solo tras superar todas las guardas (07-03).
- Mensajes accionables: qué se esperaba, qué llegó (
!r) y, si procede, qué hacer (07-03). Nonepara ausencias legítimas, excepción para contratos rotos; si conviven, que sea con criterio, comobuscar_libro()/obtener_libro()(07-03, 07-04).- Jerarquía propia con atributos: el
exceptdistingue tu error del resto y el código lee datos, no strings (07-04). - Log en la frontera: registra con
logger.exception()donde manejas, no en cada nivel intermedio — un error, una entrada en el log, no cinco (07-05).
Y el cierre práctico: caja.py juntando las tres piezas del módulo.
# caja.py — el programa que Ana ejecuta cada mañana
import logging
from pathlib import Path
from almacen import cargar_catalogo, guardar_catalogo, vender
from errores import ErrorPapyrus, LibroNoEncontradoError, StockInsuficienteError
BASE = Path(__file__).parent
(BASE / "datos").mkdir(exist_ok=True)
logging.basicConfig(
filename=BASE / "datos" / "papyrus.log",
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
encoding="utf-8",
)
logger = logging.getLogger(__name__)
def pedir_entero(mensaje): # la deuda de M1, saldada en 07-02
while True:
texto = input(mensaje)
try:
return int(texto)
except ValueError:
print(f"'{texto}' no es un número entero. Inténtalo de nuevo.")
def main():
logger.info("Papyrus abre la caja")
catalogo = cargar_catalogo() # EAFP por dentro (07-02)
while True:
titulo = input("\nTítulo (o 'fin'): ").strip()
if titulo.casefold() == "fin":
break
unidades = pedir_entero("Unidades: ")
try:
importe = vender(catalogo, titulo, unidades)
except LibroNoEncontradoError as e: # hoja: reacción específica (07-04)
print(f"No tenemos '{e.titulo}'. ¿Lo encargamos?")
logger.warning("Venta fallida, título desconocido: %r", e.titulo)
except StockInsuficienteError as e: # hoja: atributos legibles (07-04)
print(f"Solo quedan {e.disponible} de '{e.titulo}'.")
logger.warning("Stock corto: %s", e)
except ErrorPapyrus as e: # base: red del negocio (07-04)
print(f"Operación no realizada: {e}")
logger.error("Error de negocio no específico: %s", e)
else: # camino feliz fuera del try (07-02)
print(f"Venta realizada: {importe:.2f} € (IVA incluido)")
logger.info("Venta: %s x%d, %.2f €", titulo, unidades, importe)
guardar_catalogo(catalogo)
logger.info("Papyrus cierra la caja")
if __name__ == "__main__":
try:
main()
except Exception: # ÚNICA frontera con Exception (regla 3)
logger.exception("Error no previsto: la caja se cierra de emergencia")
print("Ha ocurrido un error inesperado. Consulta datos/papyrus.log")
raise SystemExit(1)Fíjate en la arquitectura de tres anillos: las hojas de la jerarquía se manejan junto al mostrador (reacción específica, WARNING), la base ErrorPapyrus hace de red del negocio, y un único except Exception en la puerta del programa — con logger.exception() y un mensaje honesto a pantalla — caza lo imprevisto sin silenciarlo. print() para Ana, logging para el diagnóstico: cada mensaje, a su público.
Queda una mención honrada: en sistemas que corren semanas, papyrus.log crece sin límite. El propio logging trae la solución — handlers como RotatingFileHandler, que parte el log por tamaño o por fecha, y configuración desde fichero — pero es tema avanzado que excede lo esencial; te bastará saber que existe y dónde buscarlo (logging.handlers) cuando la tienda lo necesite.
Errores Comunes y Consejos
- Llamar a
basicConfig()en cada módulo. Solo el programa principal configura; los módulos crean sugetLogger(__name__)y emiten. Además,basicConfigsolo surte efecto la primera vez: las llamadas repetidas se ignoran en silencio y confunden. logger.error(e)dentro delexcepten lugar delogger.exception(...). Guarda el mensaje pero tira el traceback, que era la mitad valiosa. Dentro deexcept, casi siempreexception().- Registrar y creer que ya está manejado. El log es memoria, no decisión: tras registrar, decide — relanza, aplica un valor por defecto o aborta. Registrar-y-tragar es el antipatrón
passcon mejor vestuario. - Registrar el mismo error en cada nivel de la pila. Cinco entradas idénticas para un solo fallo ensucian el diagnóstico. Regla 10: se registra en la frontera donde se maneja.
- Datos sensibles en el log. Revisa cada
logger.*como si el archivo fuera a leerlo un desconocido — porque tarde o temprano lo hará. Códigos internos sí; identidades, tarjetas y secretos, nunca. - Consejo: durante el desarrollo, cambia el umbral a
level=logging.DEBUGy añadeDEBUGgenerosos en la lógica delicada (normalización de claves, conversiones). En producción, umbral aINFO: losDEBUGsiguen ahí, dormidos y gratis. - Consejo: abre
papyrus.logde vez en cuando aunque nada falle. Un log que solo se lee en las emergencias suele descubrirse vacío, mal formateado o mudo justo cuando más falta hace.
Ejercicios
-
cierre_de_caja()con telemetría. Adapta la versión de 07-02 para que use un logger de módulo: cada fila corrupta aWARNINGcon la fila completa (%r), el total del día aINFO, y anteFileNotFoundErrorunINFO("sin ventas hoy") — ¿por quéINFOy noERROR? Losprint()de resumen para Ana se mantienen: razona qué mensaje pertenece a cada canal. -
La copia nocturna que avisa. Envuelve la
copia_de_seguridad()del módulo 6 (que copiacatalogo.jsonacopias/catalogo-<fecha>.jsonconshutil.copy2) para que: registre aINFOel destino si todo va bien; y anteOSErroruselogger.exception()y relance. Explica por qué relanzar en vez de tragarse el error, siendo "solo" una copia. -
Auditoría del decálogo. Este fragmento infringe al menos cuatro reglas del decálogo. Identifícalas por número y reescríbelo:
def alta_socio(socios, nombre, codigo): try: logging.basicConfig(level=logging.DEBUG) if codigo in socios: return False socios.append(codigo) logging.info("Alta de socio: " + nombre + " con tarjeta 4779-1234-5678-9010") return True except: pass
Soluciones
-
logger = logging.getLogger(__name__) def cierre_de_caja(): ruta = BASE / "datos" / "ventas.csv" total, ventas_ok, corruptas = 0.0, 0, 0 try: with open(ruta, encoding="utf-8", newline="") as f: for fila in csv.DictReader(f): try: total += float(fila["importe"]) except ValueError: corruptas += 1 logger.warning("Fila ilegible en ventas.csv: %r", fila) else: ventas_ok += 1 except FileNotFoundError: logger.info("Cierre sin ventas: no existe %s", ruta) print("Sin ventas registradas hoy.") return 0.0 logger.info("Cierre de caja: %d ventas, %d corruptas, total %.2f €", ventas_ok, corruptas, total) print(f"Cierre de caja: {ventas_ok} ventas, total {total:.2f} €") return totalINFOy noERRORporque un día sin ventas es un resultado legítimo del negocio (el criterio de 07-03 aplicado a niveles de log): nadie tiene que "actuar hoy". Canales: elprintda a Ana el resumen que necesita en el momento; el log guarda además las filas corruptas exactas y el desglose, que es lo que el diagnóstico del lunes necesitará. -
def copia_de_seguridad(): origen = BASE / "datos" / "catalogo.json" destino = BASE / "copias" / f"catalogo-{date.today().isoformat()}.json" try: shutil.copy2(origen, destino) except OSError: logger.exception("Copia de seguridad fallida hacia %s", destino) raise logger.info("Copia de seguridad creada: %s", destino)Se relanza porque una copia fallida silenciada es una falsa sensación de seguridad: el día que Ana necesite restaurar desde
copias/, descubriría que llevan semanas sin crearse. Regla 5 (no silenciar) y el patrón registrar-y-relanzar de 07-03/07-05: el log conserva el traceback y quien llamó decide si abortar el cierre o avisar a gritos. -
Infracciones: regla 3 (
except:pelado), regla 5 (pass: el error desaparece, y encima la función devuelveNoneen vez deTrue/False, un tercer estado fantasma), regla 10 / patrón de configuración (basicConfigdentro de una función de biblioteca, pisando — o siendo ignorada por — la configuración del programa principal), y la norma de datos sensibles (un número de tarjeta escrito en el log). De propina: logger anónimo en vez degetLogger(__name__)y untryque abriga código que no puede fallar de forma esperable (regla 4). Reescritura:logger = logging.getLogger(__name__) def alta_socio(socios, nombre, codigo): if not nombre.strip() or not codigo.strip(): raise ValueError("nombre y código no pueden estar vacíos") # regla 6 if codigo in socios: logger.info("Alta rechazada, código duplicado: %r", codigo) return False socios.append(codigo) logger.info("Alta de socio: %s", codigo) # código interno; ni nombre ni tarjeta return True
Conclusión
El módulo 7 responde, una a una, las preguntas que dejó abierto el cierre del módulo 6. ¿Y si el archivo no está? cargar_catalogo() lo intenta al estilo EAFP y convierte el FileNotFoundError en un catálogo vacío con aviso — sin el paliativo Path.exists() ni su condición de carrera (07-01, 07-02). ¿Y si la copia está corrupta? JSONDecodeError se distingue del archivo ausente y se traduce con raise ... from e sin perder la evidencia (07-02, 07-03). ¿Y si float(fila["precio"]) recibe "gratis"? Un try mínimo salta la fila, la cuenta y la deja escrita a WARNING en papyrus.log (07-02, 07-05). ¿Y si Júlia teclea un precio imposible? Las guard clauses y el __post_init__ de M5 — que ahora sabes leer como contratos — lo rechazan con un mensaje accionable, y pedir_entero() saldó de paso la deuda que int(input()) arrastraba desde el módulo 1 (07-03, 07-02). Por el camino, Papyrus ganó un vocabulario propio de errores — ErrorPapyrus y sus hojas con atributos, en errores.py — y una caja de tres anillos que atrapa lo específico junto al mostrador, lo genérico en la frontera y lo deja todo anotado con logger.exception() (07-04, 07-05). La tienda ya no se cae: se explica. El módulo 8 cambia de tercio: el código de Papyrus es correcto y robusto, y ahora toca hacerlo más expresivo y más eficiente — type hints que documentan los contratos que este módulo defendía a mano, decoradores que envuelven funciones sin repetir código, generadores que procesan ventas sin cargarlas enteras en memoria, y el momento de saldar la promesa pendiente del with: entender los context managers por dentro y, por fin, construir los tuyos.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
