Skip to content

Easy Logging

Recording when an operation starts, finishes, or fails often means repeating the same logging calls. The easy_logging module wraps Python's standard logging module with helpers for code blocks, functions, and methods.

A small real-world example

Imagine you're preparing an order summary and want to see which customer's total is being calculated.

import logging
from py_simple import log_function, log_step

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


@log_function("Calculate total for {customer}")
def calculate_total(customer: str, prices: list[float], discount: float = 0) -> float:
    return sum(prices) - discount


with log_step("Prepare order summary"):
    total = calculate_total("Ada", [20, 30], discount=5)

Example output:

INFO: Starting: Prepare order summary
INFO: Starting: Calculate total for Ada
INFO: Finished: Calculate total for Ada
INFO: Finished: Prepare order summary

What happened?

logging.basicConfig() enables INFO messages and sets their format. Configure logging once when your application starts; these helpers use the root logger and its existing configuration.

log_step() logs entry into the with block and its successful completion.

log_function() logs the function call and replaces {customer} with "Ada". Templates can use positional arguments, keyword arguments, and default values for omitted arguments. The function still returns its normal result, so total is 45. The decorator also works on synchronous methods; self and cls are excluded from template fields.

If a block or decorated function raises an exception, the helper logs an ERROR message with the traceback and re-raises the original exception. It does not log a successful completion for that operation.

Temporarily enable more detailed logging

Use temporary_log_level() to change one logger's level just for a block:

import logging
from py_simple.easy_logging import temporary_log_level

logger = logging.getLogger("example")
logger.setLevel(logging.WARNING)
with temporary_log_level(logger, "debug"):
    print(logger.isEnabledFor(logging.DEBUG))  # True
print(logger.level == logging.WARNING)  # True

The helper accepts integer levels and registered string names, including lowercase names like "debug", as setup_file_logger() does. It restores the exact previous explicit level even when the block raises. An original NOTSET level returns to NOTSET, so the logger inherits its parent's current level again. Nested blocks restore the level active at each entry. Exceptions from the block propagate unchanged.

This changes only the logger's level. Handler thresholds and filters, logger filters, disabled settings and global logging settings still apply. In particular, a handler set to INFO will still discard DEBUG records; configuring that handler remains the application's responsibility. Other users of the same logger see the temporary level too, so this does not provide thread isolation.

Why use these helpers?

These helpers handle the repeated start, success, and failure logging around an operation, keeping the code focused on its task. Use log_step() for a selected block and log_function() when every call to a synchronous function or method should be logged.

See the Easy Logging reference for argument details and examples alongside their standard-library equivalents.

Further reading

For the reasoning behind these helpers and a walkthrough of using context managers and decorators for logging, read How to Add Logging Without Cluttering Your Code by SpaceShaman.