A flexible, zero-boilerplate logging decorator built on Python’s standard logging module.
logwrap allows you to log:
- Before a function is called
- After a function returns
- When an exception occurs
All without modifying the function body.
Logging function behavior often leads to:
- Repetitive boilerplate
- Inconsistent log formats
- Scattered
print()statements - Manual
try/exceptlogging blocks
logwrap solves this by providing:
- Declarative logging
- Centralized formatting
- Dynamic message templating
- Full compatibility with existing
loggingconfiguration
- Works with any function or method
- Uses standard
logging - Supports sync and async functions
- Dynamic message templating
- Per-stage configuration (before / after / exception)
- Optional conditional logging via predicates
- Zero overhead when a stage is disabled
No extra dependencies required.
from classmods import logwraplogwrap supports all standard logging levels:
LOG_LEVEL = [
'CRITICAL',
'ERROR',
'WARNING',
'INFO',
'DEBUG',
'NOTSET',
]You may also pass integer levels (e.g. logging.DEBUG).
Invalid levels automatically fall back to a safe default.
Log messages use Python string formatting with the following variables:
| Variable | Description |
|---|---|
{func} |
Function name |
{args} |
Positional arguments (tuple) |
{kwargs} |
Keyword arguments (dict) |
{result} |
Return value (after stage only) |
{e} |
Exception object (exception stage only) |
@logwrap(before=True, after=True)
def greet(name: str):
return f"Hello {name}"
greet("Mohammad")Logs:
DEBUG - Calling greet - kwargs={'name': 'Mohammad'}
INFO - Function greet ended. result=Hello Mohammad
@logwrap(
before=('INFO', 'Starting {func} with args={args}'),
after=('INFO', '{func} completed successfully')
)
def process(data):
return len(data)@logwrap(on_exception=True)
def explode():
raise RuntimeError("Boom")Logs:
ERROR - Error in explode: Boom
@logwrap(
on_exception=('CRITICAL', 'Fatal error in {func}: {e}')
)
def critical_section():
raise ValueError("Invalid state")You can enable logging for only one phase:
@logwrap(after=True)
def compute():
return 42If an option is set to a negative value, that stage is skipped:
@logwrap(before=False, after=True)
def silent_start():
return "Done"Supported skip values:
FalseNone
Passing True uses the following defaults:
| Stage | Level | Message |
|---|---|---|
| before | DEBUG | Calling {func} - kwargs={kwargs} |
| after | INFO | Function {func} ended. result={result} |
| on_exception | ERROR | Error in {func}: {e} |
You may provide a predicate function to conditionally log:
def only_large_inputs(ctx):
return ctx["kwargs"].get("size", 0) > 100
@logwrap(
before=('DEBUG', 'Large input detected', only_large_inputs)
)
def process(size: int):
...Predicate signature:
Callable[[dict[str, Any]], bool]The predicate receives the formatting context.
logwrap automatically detects async functions:
@logwrap(before=True, after=True)
async def fetch_data():
...No configuration changes required.
logwrap works seamlessly with class methods:
class Service:
@logwrap(before=True, after=True)
def start(self, port: int):
return f"Started on {port}"You may specify a logger explicitly:
@logwrap(before=True, logger="myapp.service")
def run():
...Accepted values:
None(default: module logger)str(logger name)logging.Loggerinstance
logwrap can optionally log the execution time of a function.
- Enable it by passing the
timingargument. - Timing logs appear after function execution and have their own formatting.
- Works for both sync and async functions.
@logwrap(before=True, after=True, timing=True)
def compute_heavy(x: int):
total = sum(i * i for i in range(x))
return total
compute_heavy(10000)Logs:
DEBUG - Calling compute_heavy - kwargs={'x': 10000}
INFO - Function compute_heavy ended. result=333283335000
DEBUG - Function compute_heavy executed in 0.005432s
timingusesDEBUGby default, but you can customize it:
@logwrap(timing=('INFO', '{func} took {duration:.4f}s'))
def fast_task():
...{duration}is automatically available in the template and represents elapsed time in seconds.- Timing logging is optional. If not enabled (
timing=None), no timing data is collected. - Timing logs are separate from
afterlogs to keep function result logs and performance logs distinct. - Works with both sync and async functions.
- By default, logwrap removes self and cls from logged arguments when decorating instance methods or class methods.
- This behavior applies only when the first parameter is named self or cls.
- This keeps logs clean and focused on meaningful parameters.
- You can disable this behavior by setting strip_self=False.
class MyList:
_list = []
@logwrap(before=True, strip_self=False)
def append(self, value: Any):
self._list.append(value)
my_list = MyList()
my_list.append(1)Logs:
DEBUG - Calling append - kwargs={'self': <MyList object>, 'value': 1}
- Ultra-hot code paths
- Tight loops
- Functions called thousands of times per second
- When logging is globally disabled anyway
In those cases, inline logging may be more efficient.
- Explicit over implicit
- Logging must not change program behavior
- Configuration over repetition
- Runtime-safe by default
logwrap is intentionally:
- Non-invasive
- Fail-safe
- IDE-friendly
- Compatible with any logging setup
logwrap is ideal when you want:
- Clean function bodies
- Consistent, structured logs
- Powerful debugging hooks
- Zero boilerplate
It scales from small scripts to large production systems without getting in your way.