Usage guide

Installation

Install a local checkout with python -m pip install .. For the public distribution, use python -m pip install python-traceprint. Python imports remain traceprint. To develop or build these docs, run uv sync --dev.

Tracing print output

Passing the tracer as file avoids changing process streams:

from traceprint import Tracer

tracer = Tracer(use_colors=False, use_tabulation=True)
print("starting", file=tracer)
with tracer.span("load"):
    print("loaded", 42, "records", file=tracer)

The default prefix includes a UTC timestamp, cumulative time since construction, elapsed time since the last formatted message and the full label path. Elapsed times use a monotonic performance clock. Metadata is added once per nonblank output line. Blank lines pass through, partial writes appear immediately and flush() does not end the current line. Use a newline to begin a fresh prefix.

To intercept ordinary print(), install explicitly and always restore streams:

tracer.install(stdout=True, stderr=False)
try:
    print("hello", "world")
finally:
    tracer.uninstall()

stderr=True routes intercepted stderr to the same tracer sink. There is no independent stderr formatting path. Avoid overlapping installations by multiple tracers: restoration uses the streams captured by each installation.

Spans and decorators

span() increases depth and adds a label inside its context, then restores them on exit, including when the body raises. push() and pop() provide manual equivalents when a context manager is inconvenient.

from traceprint import trace

@trace(tracer_instance=tracer, label="transform")
def transform() -> None:
    print("transformed", file=tracer)

transform()

Without a label, the decorator uses the function name. Without a tracer_instance, it uses the package’s global tracer. The decorator wraps synchronous function calls; it does not track an async coroutine’s execution across awaits or a generator’s iteration. Context state belongs to the tracer, not an individual thread or async task. Keep tracer configuration stable while a span or decorated call is active.

Configuration

tracer.configure(
    use_colors=False,
    use_tabulation=True,
    print_transitions=True,
    filter_levels=range(0, 3),
    output_format="[{label_full}] {original_message}",
)

use_colors=None checks whether the output stream is a TTY. Explicit True or False overrides detection. use_tabulation=True inserts four spaces per depth level. With the default metadata color, indentation precedes the message; with metacolor=None, it precedes the whole formatted line. Colors do not change indentation placement.

filter_levels accepts allowed depths as a range or iterable, or a predicate that returns True for allowed depths. None allows all depths. Filtering suppresses output rather than changing nesting. disable() forwards raw text and prevents depth changes; enable() resumes formatting. lock() prevents depth changes until unlock().

The custom format accepts timestamp, cumulative, elapsed, label, label_full and original_message. Include original_message to retain the text. Timestamp syntax follows datetime.strftime. label is the last label and label_full is the slash-separated stack; regular labeled messages add />. Timing advances when a message is formatted, including direct format() calls.

The format is applied to the first nonblank fragment of a line; subsequent fragments are appended immediately. Prefer prefix-based formats for incremental output, since a custom suffix would follow that first fragment.

Logging

Use a dedicated logger to avoid changing the application’s root logging setup:

import logging
from traceprint import TracerHandler

logger = logging.getLogger("example")
handler = TracerHandler(tracer_instance=tracer)
handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.propagate = False
try:
    logger.info("loaded %s records", 42)
finally:
    logger.removeHandler(handler)
    handler.close()

LoggerMixin provides a class-named logger and a self.log proxy controlled by verbose. The transformer example shows it with empty scikit-learn-style base classes. logging_passthrough=True attaches a handler targeting the global tracer and disables propagation; this changes the shared class logger’s handlers. Leave it off when the application configures logging itself.

Files

Mirror tracer output to a file, stripping terminal escape sequences:

from traceprint import TracerFileHandler

file_handler = TracerFileHandler("trace.log", mode="a")
tracer.add_file_handler(file_handler)
try:
    print("saved", file=tracer)
    tracer.flush()
finally:
    tracer.remove_file_handler(file_handler)
    file_handler.close()

Alternatively, attach TracerFileHandler(..., tracer_instance=tracer) directly to a logger. It formats each record for the file. Use one route for a given record to avoid duplicates. File logging and mirroring strip ANSI escapes. Closing a handler owns file cleanup; uninstalling a tracer only restores process streams. Raw output while the tracer is disabled bypasses mirrored file handlers.

Global labels

TRACEPRINT_INITIAL_LABELS=job/load seeds the global tracer’s label stack at import time. Empty slash-separated components are ignored. It does not install the tracer. Explicit Tracer instances do not read this variable.