▷ Logging en Python — Deja de usar `print` para depurar 2026

Logging en Python — Deja de usar `print` para depurar

Si llevas tiempo programando, probablemente sigues llenando el código de print(...) para depurar. Y luego los borras antes del commit. Y luego los vuelves a poner cuando hay un bug. Y vuelta a empezar.

Esto funciona para juguetes, pero a la mínima que tu código entra en algo más serio (script en cron, API en producción, web en Flask, automatización que corre cada noche), print se queda corto. No tiene niveles, no tiene timestamps, no se redirige fácil, no se silencia, no se filtra, y no separa “info útil” de “ruido”.

La buena noticia: Python trae logging en la stdlib, viene de fábrica con prácticamente todo lo que necesitas, y configurarlo bien para un proyecto te lleva 5 minutos. En esta entrada te enseño los conceptos clave, configuraciones realistas, los errores típicos al empezar y por qué nunca volverás al print para depurar.

Por qué print no escala

Tres motivos prácticos:

  1. No tiene niveles. Todo se imprime. Si el script imprime 200 cosas útiles cuando va bien, no ves los errores entre el ruido.
  2. No se redirige limpiamente. Si quieres logs en fichero + en consola simultáneamente, te toca duplicar todo y pegar with open(..., "a") as f: en cada print.
  3. Se queda en el código. Olvidas un print("aquí 1") y va a producción. Felicidades.

El módulo logging resuelve los tres. Tienes 5 niveles (DEBUG, INFO, WARNING, ERROR, CRITICAL), puedes configurar dónde van los logs (consola, fichero, syslog, email, Sentry…), filtras por nivel, formateas con timestamps y nombre de módulo, y silencias todo de una sola vez subiendo el nivel global.

Setup mínimo que ya marca diferencia

import logging

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

logger = logging.getLogger(__name__)

logger.debug("Esto no se ve (nivel está en INFO)")
logger.info("Esto sí")
logger.warning("Algo raro pasó")
logger.error("Esto sí preocupa")
logger.critical("Esto está ardiendo")

Output:

2026-04-19 15:30:00,123 [INFO] __main__: Esto sí
2026-04-19 15:30:00,124 [WARNING] __main__: Algo raro pasó
2026-04-19 15:30:00,125 [ERROR] __main__: Esto sí preocupa
2026-04-19 15:30:00,126 [CRITICAL] __main__: Esto está ardiendo

Tres minutos de configuración y ya tienes timestamp, nivel, nombre de módulo y mensaje. Cualquier print queda en evidencia.

💡 Patrón pro: un logger por módulo, con getLogger(__name__). Así cada log lleva el nombre del fichero que lo emitió y puedes filtrar/silenciar por módulo después.

Los 5 niveles de logging y cuándo usarlos

NivelCuándo
DEBUGDetalles internos para diagnóstico. Solo en desarrollo.
INFOEventos normales que confirman que la app va bien (arranque, request servida).
WARNINGAlgo extraño pero no rompe (uso de API deprecada, fallback).
ERRORFalló algo y la operación no se completó, pero la app sigue.
CRITICALFallo grave: la app no puede continuar.

El nivel del logger filtra: si pones level=INFO, los DEBUG no salen. Si pones level=WARNING, solo ves warnings y arriba.

Tip-friki: en producción típicamente INFO o WARNING. En desarrollo, DEBUG. Lo configuras con una variable de entorno y arreglado.

📥 Llévate el cheatsheet de Python (gratis)

PDF de 6 páginas con lo esencial: tipos, condicionales, bucles, estructuras de datos, funciones y los errores que más vas a cometer. Para tener al lado mientras programas.

Sin spam. Te apuntas a la lista, descargas el cheatsheet y recibes contenido de Python cada semana.

Logger por módulo (el patrón pro)

En vez de un logging.info(...) global, cada fichero tiene su propio logger nombrado:

# en facturacion.py
import logging
logger = logging.getLogger(__name__)

def cobrar(usuario, monto):
    logger.info("Cobrando %s€ a usuario %s", monto, usuario.id)
    ...
    logger.warning("Tarjeta caducada en 30 días para %s", usuario.id)
# en main.py
import logging
logging.basicConfig(level=logging.INFO)

# Silenciar logs de un módulo concreto
logging.getLogger("requests").setLevel(logging.WARNING)

getLogger(__name__) te da un logger cuyo nombre es el del módulo (mi_paquete.facturacion). Ventajas:

  • Cada log identifica de dónde viene.
  • Puedes silenciar/elevar nivel por módulo: típicamente bajas tu propio código a DEBUG y subes a WARNING los de librerías ruidosas como requests, urllib3, boto3.

💡 Notación lazy: logger.info("X = %s", x) es mejor que logger.info(f"X = {x}") cuando hay logs de DEBUG en hot paths. La librería solo construye el string si el nivel está activo. Con f-strings, siempre se construye.

Formato más completo

format admite muchos atributos. Los más útiles:

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)-8s] %(name)s:%(lineno)d - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

Atributos:

PlaceholderQué muestra
%(asctime)sTimestamp legible
%(levelname)sNivel: INFO, WARNING…
%(name)sNombre del logger (__name__)
%(funcName)sFunción que emitió el log
%(lineno)dLínea del fichero
%(message)sEl mensaje en sí
%(pathname)sRuta completa del fichero

%(levelname)-8s con -8 te alinea el nivel a 8 caracteres (queda más legible cuando alternas INFO/WARNING/ERROR).

Loggear a fichero

basicConfig admite filename:

logging.basicConfig(
    filename="app.log",
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
)

Para tenerlo a la vez en fichero y en consola, ya necesitas handlers:

import logging

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

# Handler 1: consola con INFO+
consola = logging.StreamHandler()
consola.setLevel(logging.INFO)
consola.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))

# Handler 2: fichero con DEBUG+
archivo = logging.FileHandler("app.log")
archivo.setLevel(logging.DEBUG)
archivo.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s"))

logger.addHandler(consola)
logger.addHandler(archivo)

Resultado: en consola ves solo INFO/WARNING/ERROR (limpio). En fichero, todo desde DEBUG (forensic).

Casos reales típicos

Logear excepciones con su traceback

try:
    procesar(datos)
except Exception:
    logger.exception("Fallo al procesar")

logger.exception() se usa dentro de except. Loggea como ERROR e incluye el traceback completo. Es el patrón estándar cuando capturas para no romper la app pero quieres dejar constancia.

Rotación de ficheros para que no crezca infinito

from logging.handlers import RotatingFileHandler

handler = RotatingFileHandler(
    "app.log", maxBytes=5_000_000, backupCount=3
)

Cuando app.log llega a 5 MB, lo renombra a app.log.1 (y el viejo .1 pasa a .2, etc.) y empieza un fichero nuevo. Mantiene 3 backups. Sin esto, en producción, tu fichero de logs te llena el disco.

💡 ¿Vas a deployar a un VPS? Mira scripts y if __name__ para entry points limpios y scripts-python-automatizar para automatización con cron.

Configurar desde fichero/dict (proyecto serio)

Para proyectos medianos en adelante, en vez de configurar en el main.py, usas un dict de configuración:

import logging.config

CONFIG = {
    "version": 1,
    "formatters": {
        "default": {
            "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        },
    },
    "handlers": {
        "consola": {"class": "logging.StreamHandler", "formatter": "default"},
        "archivo": {
            "class": "logging.handlers.RotatingFileHandler",
            "filename": "app.log",
            "maxBytes": 5_000_000,
            "backupCount": 3,
            "formatter": "default",
        },
    },
    "loggers": {
        "": {"handlers": ["consola", "archivo"], "level": "INFO"},
        "requests": {"level": "WARNING"},
    },
}

logging.config.dictConfig(CONFIG)

Esto es lo más estándar en proyectos profesionales. El dict puede venir de un YAML/JSON externo si quieres.

Logs estructurados para analizar después

Si tu app va a producción y quieres analizar los logs (con Datadog, Loki, ELK…), pásalos a JSON. Hay librerías (python-json-logger) o lo haces a mano:

import json
import logging

class JsonFormatter(logging.Formatter):
    def format(self, record):
        return json.dumps({
            "ts": self.formatTime(record),
            "level": record.levelname,
            "module": record.name,
            "msg": record.getMessage(),
        })

Cada línea de log es un JSON parseable. Mucho más útil que un string libre.

Errores típicos al usar logging

# 1. Usar `print` en producción
print("Error: usuario no encontrado")   # ❌
logger.error("Usuario no encontrado: id=%s", id)   # ✓

# 2. f-strings en logs ruidosos
logger.debug(f"Estado: {calcular_estado_caro()}")
# ❌ se ejecuta calcular_estado_caro SIEMPRE, aunque DEBUG esté off

logger.debug("Estado: %s", calcular_estado_caro())
# ⚠️ igual, se ejecuta antes de pasarlo. Para casos caros:
if logger.isEnabledFor(logging.DEBUG):
    logger.debug("Estado: %s", calcular_estado_caro())   # ✓ se evalúa solo si toca

# 3. Loggear desde un script lanzado solo con prints "porque ya tengo logs"
print("INFO: arrancando")           # ❌ está bien sólo si tu script realmente no necesita logs
# Si vas a tener bugs en producción → usa logging desde el inicio.

# 4. Capturar excepción y loggear sin traceback
try:
    procesar()
except Exception as e:
    logger.error(f"Error: {e}")     # ❌ pierdes el traceback
    # Mejor:
    logger.exception("Error procesando")   # ✓ incluye traceback

# 5. logger.basicConfig() después de loggear ya
logger = logging.getLogger(__name__)
logger.info("antes")        # crea config por defecto
logging.basicConfig(...)   # ❌ ya no surte efecto en muchos casos
# Configura ANTES de loggear nada.

# 6. Pegar logger.info a un nivel muy alto sin saberlo
logging.basicConfig(level=logging.WARNING)
logger.info("nada se ve")    # ❌ filtrado por nivel

Resumen

ConceptoRegla
Logger por módulologger = logging.getLogger(__name__) arriba de cada fichero
NivelesDEBUG / INFO / WARNING / ERROR / CRITICAL — filtra por nivel global
Setup mínimologging.basicConfig(level=..., format=...)
Mensaje con argslogger.info("X=%s", x) (lazy, evita f-string en hot paths)
Excepcioneslogger.exception("...") dentro del except (incluye traceback)
Consola + ficheroHandlers separados con sus niveles y formatos
RotaciónRotatingFileHandler para que el fichero no crezca infinito
ProduccióndictConfig(...) con un dict por entorno
Lo más importanteConfigura una vez al inicio y usa loggers nombrados en todos lados

¿Te ha valido esto?

Si te ha resultado útil, llévate el cheatsheet de Python en PDF — 6 páginas con tipos, condicionales, bucles, estructuras de datos, funciones y los errores típicos. Para tener al lado mientras programas. Gratis.

Sin spam. Email + cheatsheet + un correo por semana con tutoriales nuevos.

Tu siguiente paso

Si has llegado hasta aquí, ya no vas a volver al print. La próxima vez que arranques un script que vaya a vivir más de un día, copias el setup mínimo, defines tu logger, y a otra cosa. Y cuando llegue la hora de meter rotación, fichero, JSON estructurado o Sentry — tienes la base para crecer sin reescribir.

Si quieres aprender Python desde la base hasta proyectos reales con producción, despliegue, observabilidad y automatización, en El Pythonista lo enseño paso a paso.

Un abrazo,
Oscar

¿Quieres aprender Python en orden, no a saltos?

Esto que has leído es solo una pieza. En El Pythonista lo verás todo encadenado: 11 módulos, 37+ horas de vídeo, 734 actividades y un proyecto real (MovieTracker) que crece contigo desde la primera variable hasta el deploy a producción.

Ver el curso completo →

37+ horas · 734 actividades · Proyecto real · Acceso de por vida · 14 días de garantía

Compartir

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Publicar un comentario