24. Logging, mensajes de error y salida por consola con criterio profesional

24.1 Objetivo del tema

La salida de un programa también forma parte de su calidad. Un mensaje confuso, un print olvidado o un log con datos incompletos puede hacer difícil entender qué ocurrió cuando aparece un problema.

En este tema veremos cuándo usar print, cuándo usar logging, cómo escribir mensajes de error útiles y cómo separar salida de usuario, diagnóstico técnico y reglas de negocio.

Objetivo práctico: reemplazar salidas improvisadas por mensajes claros y logging útil sin mezclar diagnóstico con lógica principal.

24.2 Print no siempre está mal

print es útil para scripts simples, ejemplos didácticos o salida final para el usuario. El problema aparece cuando se usa como mecanismo de diagnóstico permanente dentro de funciones de negocio.

def calcular_total(productos):
    total = sum(producto.precio * producto.cantidad for producto in productos)
    print("total calculado", total)
    return total

Ese print puede ensuciar pruebas, consola y procesos automáticos. Si necesitamos diagnóstico, conviene usar logging.

24.3 Separar cálculo y salida

Una función de cálculo debería devolver datos. Otra función puede encargarse de mostrar.

def calcular_total(productos):
    return sum(producto.precio * producto.cantidad for producto in productos)


def mostrar_total(total):
    print(f"Total: {total:.2f}")

Esto facilita probar el cálculo sin capturar salida por consola.

24.4 Introducción a logging

El módulo logging permite registrar eventos con niveles y configuración. A diferencia de print, puede activarse, desactivarse, redirigirse a archivos o mostrar más contexto.

import logging

logger = logging.getLogger(__name__)


def calcular_total(productos):
    total = sum(producto.precio * producto.cantidad for producto in productos)
    logger.info("Total calculado: %.2f", total)
    return total

24.5 Configuración básica

En un script o punto de entrada, puedes configurar logging así:

import logging

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

Normalmente la configuración se hace una sola vez en el punto de entrada, no dentro de cada módulo.

24.6 Niveles de logging

Los niveles más usados son:

  • DEBUG: información detallada para diagnóstico.
  • INFO: eventos normales importantes.
  • WARNING: situación extraña, pero recuperable.
  • ERROR: error que impide completar una operación.
  • CRITICAL: error grave que compromete el sistema.

24.7 Usar mensajes con parámetros

Con logging conviene usar parámetros en lugar de construir strings siempre.

logger.info("Venta procesada: cliente=%s total=%.2f", cliente, total)

Evita esto si el mensaje solo sirve para logging:

logger.info(f"Venta procesada: cliente={cliente} total={total}")

La versión con parámetros deja que logging maneje el formateo cuando corresponde.

24.8 Logging de excepciones

Dentro de un bloque except, logger.exception registra el traceback automáticamente.

def cargar_productos(ruta):
    try:
        with open(ruta, encoding="utf-8") as archivo:
            return archivo.readlines()
    except OSError:
        logger.exception("No se pudo leer el archivo de productos: %s", ruta)
        raise

Registramos el error y volvemos a lanzarlo. No lo ocultamos.

24.9 Mensajes de error útiles

Un mensaje de error útil debe explicar qué falló y, si corresponde, qué dato provocó el problema.

raise ValueError("La cantidad debe ser positiva")

Más contextual:

raise ValueError(f"La cantidad debe ser positiva: {cantidad}")

Evita mensajes como "error", "falló" o "datos incorrectos" si puedes ser más preciso.

24.10 No exponer detalles innecesarios al usuario

El mensaje técnico para logs no siempre debe ser el mismo que ve el usuario. Un traceback completo puede ser útil para el programador, pero confuso para una persona que usa el sistema.

try:
    total = calcular_total(productos)
except ValueError as error:
    logger.warning("Datos inválidos al calcular total: %s", error)
    print("No se pudo calcular el total. Revisa los productos ingresados.")

El log conserva detalle técnico; la consola muestra una acción comprensible.

24.11 Evitar logs excesivos

Registrar todo puede ser tan malo como no registrar nada. Los logs excesivos dificultan encontrar lo importante.

logger.debug("Entrando a calcular_total")
logger.debug("Producto 1")
logger.debug("Producto 2")
logger.debug("Saliendo de calcular_total")

Prefiere mensajes que aporten contexto real ante problemas o eventos importantes.

24.12 Evitar datos sensibles

No registres contraseñas, tokens, datos personales sensibles o información que no debería quedar almacenada.

# Evitar:
logger.info("Login usuario=%s password=%s", email, password)

Mejor:

logger.info("Intento de login para usuario=%s", email)

24.13 Aplicación sobre ventas_demo

En ventas_demo, la función de cálculo no debería imprimir. Si queremos registrar eventos, podemos usar logging:

import logging

logger = logging.getLogger(__name__)


def calcular_total_venta(venta):
    subtotal = calcular_subtotal(venta.productos)
    logger.debug("Subtotal calculado: %.2f", subtotal)
    descuento = obtener_descuento(venta.cliente.tipo)
    impuesto = obtener_impuesto(venta.pais)
    total = subtotal * (1 - descuento) * (1 + impuesto)
    return round(aplicar_envio(total), 2)

El log está en nivel DEBUG porque es información de diagnóstico, no salida normal para el usuario.

24.14 Punto de entrada con configuración

Un script principal puede configurar logging y mostrar resultados.

import logging

from ventas import calcular_total_venta


def main():
    logging.basicConfig(level=logging.INFO)
    total = calcular_total_venta(crear_venta_demo())
    print(f"Total final: {total:.2f}")


if __name__ == "__main__":
    main()

La configuración y la salida de usuario quedan en el punto de entrada, no dentro del módulo de reglas.

24.15 Probar salida por consola

Si una función tiene como responsabilidad mostrar salida, puede probarse con capsys en pytest.

def mostrar_total(total):
    print(f"Total final: {total:.2f}")


def test_mostrar_total(capsys):
    mostrar_total(1500)

    salida = capsys.readouterr()
    assert salida.out == "Total final: 1500.00\n"

Pero evita capturar salida para probar funciones que deberían ser puro cálculo.

24.16 Ejercicio guiado

Mejora este código:

def procesar_venta(productos):
    print("procesando")
    total = 0
    for producto in productos:
        if producto["cantidad"] <= 0:
            print("error")
            return 0
        total += producto["precio"] * producto["cantidad"]
    print("listo")
    return total

Una mejora posible:

import logging

logger = logging.getLogger(__name__)


def procesar_venta(productos):
    total = 0
    for producto in productos:
        if producto["cantidad"] <= 0:
            raise ValueError("La cantidad debe ser positiva")
        total += producto["precio"] * producto["cantidad"]

    logger.info("Venta procesada correctamente")
    return total

24.17 Ejercicio propuesto

En ventas_demo, realiza estas tareas:

  • Busca print dentro de funciones de cálculo o reglas.
  • Separa salida de usuario en una función o script principal.
  • Agrega logging en nivel adecuado si necesitas diagnóstico.
  • Mejora al menos dos mensajes de error.
  • Ejecuta pruebas y herramientas de calidad.
python -m ruff check src tests
python -m black src tests
python -m pytest

24.18 Lista de verificación

Antes de continuar, verifica que puedes hacer lo siguiente:

  • Diferenciar salida de usuario y logging técnico.
  • Evitar print dentro de reglas de negocio.
  • Crear un logger con logging.getLogger(__name__).
  • Elegir niveles adecuados de logging.
  • Usar logger.exception dentro de except.
  • Escribir mensajes de error específicos.
  • Evitar datos sensibles en logs.

24.19 Conclusión

En este tema vimos que la salida de un programa debe diseñarse con criterio. print sirve para salida simple, pero el diagnóstico técnico debería usar logging, niveles y mensajes útiles.

En el próximo tema trabajaremos con calidad en scripts Python: entrada, validación, funciones principales y main.