
Si tu programa necesita explicar qué ocurrió después de una ejecución, print() suele quedarse corto. No porque imprimir texto sea incorrecto, sino porque una salida de diagnóstico no trae, por sí sola, niveles de importancia, nombre de módulo, contexto de la operación ni una política para separar el flujo normal de los fallos. La decisión útil es esta: usa print() para comunicar una salida ordinaria al usuario y usa logging cuando necesites conservar evidencia que otra persona pueda filtrar, correlacionar y revisar.
Esta diferencia evita dos extremos frecuentes: llenar la terminal de mensajes que nadie puede interpretar o registrar tantos datos que el propio log se convierte en un riesgo. La biblioteca estándar de Python ofrece loggers, handlers, formatters y filtros para construir una política gradual, mientras que las recomendaciones de seguridad recuerdan que un registro debe servir para investigar sin exponer secretos ni datos personales [1] [4].
Empieza por la decisión, no por el nivel
Antes de añadir una llamada de logging, escribe la pregunta que el registro debe ayudar a responder. En una petición HTTP puede ser “¿qué ocurrió con esta operación?”, en un proceso por lotes “¿qué elemento falló?” y en una tarea programada “¿terminó dentro del tiempo esperado?”. Si no existe una pregunta concreta, probablemente todavía no sabes qué evento registrar.
| Situación | Salida adecuada | Qué debe permitir decidir |
|---|---|---|
| Mensaje visible para quien ejecuta un script | print() | Qué resultado debe leer o copiar el usuario |
| Hito normal del programa | logger.info() | Si el flujo esperado ocurrió |
| Detalle para investigar una rama concreta | logger.debug() | Qué valores controlados explican una decisión |
| Situación inesperada pero recuperable | logger.warning() | Si conviene observar, reintentar o corregir |
| Operación que no pudo completarse | logger.error() o una excepción | Qué acción de recuperación necesita el sistema |
Los niveles no son etiquetas decorativas. La documentación oficial los describe como una escala de severidad: DEBUG para detalles de diagnóstico, INFO para confirmar funcionamiento, WARNING para una anomalía, ERROR para una operación fallida y CRITICAL para un problema grave que puede impedir continuar [2]. Elegir el nivel correcto ayuda a filtrar; no sustituye el manejo de excepciones ni una política de alertas.
Qué convierte una línea en evidencia
Una línea como validation failed informa poco. No identifica la operación, la petición, el campo afectado ni el resultado que debe revisarse. Un mensaje mejor construido responde, con la menor cantidad de datos necesaria, a cuatro preguntas: qué acción ocurrió, sobre qué entidad controlada, con qué resultado y cómo encontrar los eventos relacionados.
logger.warning(
"validation failed request_id=%s field=%s route=%s",
request_id,
"email",
"/checkout",
)El ejemplo no imprime el valor del correo electrónico; registra el nombre del campo. Esa diferencia es importante: el contexto debe ser suficiente para investigar, no una copia completa del payload. Para una operación real también pueden ser útiles la duración, el código de resultado, el nombre del módulo y un identificador de correlación que no revele información personal.
Configura el sistema una sola vez
Un patrón estable es que cada módulo obtenga su propio logger con logging.getLogger(__name__), mientras que la aplicación centraliza handlers y formato. Así, el código que emite eventos no necesita saber si terminarán en la consola, en un archivo o en un colector administrado. La documentación de Python describe esta organización jerárquica y el uso de handlers para dirigir los registros a distintos destinos [1] [3].
import logging
import logging.handlers
import os
def configure_logging() -> None:
level_name = os.getenv("LOG_LEVEL", "INFO").upper()
level = getattr(logging, level_name, None)
if not isinstance(level, int):
raise ValueError(f"Nivel inválido: {level_name}")
root = logging.getLogger()
root.setLevel(level)
console = logging.StreamHandler()
console.setLevel(level)
console.setFormatter(logging.Formatter(
"%(asctime)s %(levelname)s %(name)s: %(message)s"
))
root.addHandler(console)
configure_logging()
logger = logging.getLogger(__name__)
logger.info("service started")La configuración anterior es deliberadamente pequeña. En una aplicación que carga módulos varias veces, protege la inicialización para no añadir el mismo handler repetidamente. Si necesitas guardar eventos en archivo, un RotatingFileHandler puede limitar el tamaño de cada archivo; esa decisión debe acompañarse de permisos, retención, rotación y eliminación definidos por el entorno. Un archivo local no es automáticamente un sistema de observabilidad.
Separa consola, archivo y colector según su propósito
No todas las salidas necesitan el mismo nivel. Una configuración puede mostrar INFO en consola y conservar más detalle en un destino de diagnóstico, pero esa separación debe responder a una necesidad operativa. Si el equipo no revisa el archivo, guardar más líneas solo aumenta el volumen. Si la consola se usa para alertas, mezclar cada evento normal con un fallo dificulta la reacción.
La receta de la biblioteca estándar muestra que varios handlers pueden usar niveles y formatos distintos [3]. Antes de adoptar ese patrón, decide quién consume cada salida, cuánto tiempo se conserva, qué ocurre cuando el destino no está disponible y cómo se evita que los mensajes incluyan credenciales o datos de clientes. En contenedores, puede ser preferible escribir a la salida estándar y delegar almacenamiento y retención al entorno de ejecución.
Añade contexto sin propagar datos peligrosos
Un identificador de petición es útil cuando aparece en todos los eventos de una misma operación. En código síncrono, LoggerAdapter puede añadir campos comunes; en código asíncrono, contextvars puede conservar un valor local al contexto de ejecución. La elección depende de cómo viaja la operación entre funciones y tareas. Debes probar que dos peticiones simultáneas no compartan por error el mismo identificador.
import logging
base_logger = logging.getLogger(__name__)
logger = logging.LoggerAdapter(
base_logger,
{"request_id": "req-7f9a"},
)
logger.info("checkout started")
logger.warning("payment retry attempt=%s", 2)El formato debe incluir el campo solo si todos los registros que pasan por ese handler lo proporcionan. De lo contrario, una configuración aparentemente útil puede fallar al formatear un evento emitido por otra biblioteca. En sistemas grandes conviene acordar nombres de campos, tipos y valores ausentes antes de que cada módulo invente su propia variante.
Protege la evidencia que produces
Contraseñas, tokens, cookies de sesión, claves API, números completos de pago y payloads personales no deben aparecer en un log por defecto. OWASP recomienda definir qué eventos se registran, limitar los datos sensibles, proteger el acceso, considerar integridad y disponibilidad y evitar que los archivos queden expuestos desde una ruta web [4]. Estas recomendaciones son generales: la política concreta también debe respetar el contexto y las obligaciones aplicables al sistema.
El modo DEBUG merece una condición de salida. Puede servir durante una investigación breve, pero dejarlo activo indefinidamente aumenta volumen, costo, latencia, retención y superficie de exposición. Si necesitas registrar un dato de negocio, prefiere una versión redactada o un identificador controlado. Documenta quién puede elevar el nivel, durante cuánto tiempo y cómo se revierte el cambio.
Registra excepciones donde exista contexto suficiente
Una excepción y un log cumplen funciones diferentes. La excepción comunica que una operación no pudo continuar; el log ayuda a reconstruir el contexto. Dentro de un bloque except, logger.exception() conserva la traza mientras el evento se emite. Regístrala en la capa que conoce la operación y evita volver a registrar la misma traza en cada nivel de la pila.
try:
process_event(event)
except TimeoutError:
logger.exception(
"event processing failed event_id=%s",
event.id,
)
raiseEl identificador del evento del ejemplo debe ser seguro para aparecer en la salida. Si el error se recupera con un reintento, registra la decisión y su límite; si se propaga, deja claro qué componente tendrá la responsabilidad siguiente. Para aprender a separar el registro del manejo de errores, puedes consultar la guía interna sobre manejo de excepciones en Python y la explicación sobre cómo leer errores y tracebacks de Python.
Evita duplicados y prueba la política
Los loggers tienen una jerarquía. Un logger hijo puede propagar eventos hacia handlers de sus antecesores; si añades handlers tanto al módulo como a la raíz, el mismo registro puede aparecer dos veces. Usa un punto claro de configuración, revisa propagate cuando corresponda y no “corrijas” duplicados bajando todos los niveles.
La prueba mínima no consiste en mirar una terminal y decidir que “parece funcionar”. Comprueba que un camino normal produce INFO, que una anomalía produce WARNING, que una excepción conserva la traza, que el identificador se mantiene dentro de una operación y que ningún secreto aparece en el resultado. Si el registro se escribe en archivo o se envía a un colector, prueba también permisos, rotación, fallo del destino y comportamiento bajo volumen.
Un proyecto que guarda datos en disco puede complementar esta guía con su documentación interna sobre lectura y escritura de archivos en Python. Si el objetivo es comprobar que una operación conserva un comportamiento verificable, diseña una prueba que inspeccione nivel, mensaje, contexto y ausencia de datos sensibles; el destino de una referencia debe ser una página real y pertinente, no una ruta que redirige a la portada.
Checklist para adoptar logging sin ruido
- La aplicación define cuándo una salida es comunicación para el usuario y cuándo es evidencia para diagnóstico.
- Cada módulo obtiene su logger sin configurar handlers locales innecesarios.
- Los niveles reflejan decisiones: detalle, hito, anomalía, fallo o interrupción grave.
- Los eventos importantes incluyen operación, resultado y contexto mínimo correlacionable.
- Los secretos y datos personales se excluyen o se redactan antes de llegar al handler.
- La consola, los archivos y los colectores tienen propósitos, niveles y retenciones explícitos.
- Las excepciones se registran donde existe contexto suficiente y no se duplican sin motivo.
- Las pruebas revisan niveles, campos, trazas, duplicación, permisos y rotación.
- Un cambio temporal de nivel puede ser autorizado, auditado y revertido.
El objetivo no es reemplazar cada print(). Es reservar cada mecanismo para la pregunta que puede responder mejor. Una salida útil permite reconstruir un flujo sin inventar contexto; una política segura consigue hacerlo sin convertir la aplicación en una fuente accidental de secretos. Cuando una línea no ayuda a diagnosticar, medir o actuar, elimínala o explica por qué debe existir.
Fuentes y alcance
Este artículo resume la documentación oficial de Python y recomendaciones generales de seguridad para logging. Los ejemplos son educativos y deben probarse en el entorno y la versión de Python que utiliza tu proyecto; la configuración de producción requiere decisiones adicionales sobre colectores, permisos, retención, privacidad y operación.
- Documentación de la biblioteca
loggingde Python. - Python Logging HOWTO.
- Python Logging Cookbook.
- OWASP Logging Cheat Sheet.

Martin Rojas escribe sobre tecnología, programación y desarrollo web. En Skydutz Academy comparte explicaciones prácticas sobre herramientas digitales, conceptos de programación y recursos para aprender de forma progresiva. Su objetivo es hacer que los temas técnicos sean más claros, útiles y accesibles para lectores de distintos niveles.
