cheat sheet

loguru

Package-level reference for loguru on PyPI — install variants, version policy, and how it coexists with (does not replace) the stdlib logging module.

loguru

What it is

loguru is a structured logging library by Delgan that aims to make Python logging "stupid simple": one import, one logger object, no handlers/formatters/loggers to plumb together. It ships sensible defaults for colour, rotation, retention, compression, and exception tracebacks.

It is not a drop-in replacement for the stdlib logging module — it's a parallel system with its own API. Coexistence with libraries that already use stdlib logging requires the InterceptHandler recipe (covered in the companion article).

Install

bash
pip install loguru

Output: (none — exits 0 on success). Pure-Python, no compiled deps.

bash
uv add loguru

Output: dependency added to pyproject.toml

bash
poetry add loguru

Output: updated lockfile + virtualenv install

Versioning & Python support

  • Current stable line is 0.7.x. The project has stayed on 0.x since 2018 — the maintainer prefers 0.x to signal that minor releases may break, even though the library is widely production-used.
  • Recent releases run on Python 3.5+ (yes, still). Python 3.13+ support landed in the 0.7.x series.
  • Loose semver — minor bumps can deprecate API; check the changelog before upgrading.
  • Single maintainer with a slow but consistent release cadence (a few releases per year).

Package metadata

  • Maintainer: Delgan (single-maintainer project)
  • Project home: github.com/Delgan/loguru
  • Docs: loguru.readthedocs.io
  • PyPI: pypi.org/project/loguru
  • License: MIT
  • Governance: single maintainer (Delgan), community contributions via GitHub
  • First released: 2018
  • Downloads: millions per month — consistently in PyPI's top 200

Optional dependencies & extras

loguru is a single-package, pure-Python library with no PyPI extras. The runtime deps are minimal:

  • colorama (on Windows) — ANSI colour support
  • win32-setctime (on Windows) — file timestamp control for log rotation
  • aiocontextvars (only on Python 3.6 — vestigial)

For features users sometimes assume require extras but don't:

  • Compression (.gz, .zip, .tar, .tar.gz, etc.) — handled by stdlib gzip / zipfile / tarfile, no extra needed.
  • JSON serialisationlogger.add(..., serialize=True) works out of the box.
  • Async supportlogger.add(..., enqueue=True) makes the sink non-blocking; the library is async-safe without an [async] extra.

Alternatives

PackageTrade-off
logging (stdlib)Zero-dep, universal. Verbose to configure but every library uses it. Reach for this when shipping a library — never force loguru on downstream users.
structlogStructured-first (JSON / key-value). Composable processors pipeline. Use when log aggregation (Datadog, ELK) is the primary consumer.
picologgingC-extension fork of stdlib logging — 4–10× faster, API-compatible. Use only when stdlib logging is the bottleneck.
rich (logging handler)Beautiful console output but for display, not transport. Often paired with stdlib logging, not loguru.
eliotAction-tree logging — every log is part of a causal action. Different mental model; niche.

Common gotchas

  1. Loguru is not a stdlib logging replacement. Libraries you depend on still log via logging.getLogger(...). Those messages will not flow through loguru's sinks unless you install the InterceptHandler recipe (a ~15-line snippet from the docs).
  2. Per-record formatting is dynamic. Unlike stdlib logging's static Formatter, loguru lets format= be a callable. This is powerful but means a buggy formatter can crash every log call — keep it side-effect-free.
  3. Single global logger is by design. There's no getLogger(__name__) equivalent. Use logger.bind(name=__name__) to attach module context if you need filtering by source.
  4. Thread / async safety claims need enqueue=True. The library is described as safe for multi-threaded use, but only when the sink uses the internal queue. Without enqueue=True, two threads writing to the same file sink race on the underlying file handle.
  5. logger.add() returns a sink ID, not a handler object. To remove a sink, pass the ID to logger.remove(sink_id). Calling logger.remove() with no arguments removes all sinks — including the default stderr one, leaving a silent logger.
  6. Exception capture (@logger.catch) re-raises by default. It logs and then propagates. Pass reraise=False to swallow — but think hard before doing so; swallowed exceptions are why bugs ship.
  7. Single maintainer = single point of failure. Loguru has one primary maintainer. Release cadence is slow. For projects that need a CVE patched within hours, the stdlib's release pipeline is more predictable.

Real-world recipes

Per-environment sinks

python
import sys
import os
from loguru import logger

logger.remove()  # drop the default stderr sink

if os.getenv("ENV") == "production":
    logger.add(
        "/var/log/app/{time:YYYY-MM-DD}.log",
        rotation="100 MB",
        retention="14 days",
        compression="gz",
        serialize=True,        # JSON output
        enqueue=True,          # async/safe across workers
        level="INFO",
    )
    logger.add(sys.stderr, level="WARNING", format="{level} {message}")
else:
    logger.add(sys.stderr, level="DEBUG", colorize=True,
               format="<green>{time:HH:mm:ss}</green> <level>{level}</level> {message}")

Production sinks emit JSON to disk (parseable by log aggregators) and a coloured stderr stream for human eyes during incident triage. Development gets a single coloured stream at DEBUG level. The remove() call before adding sinks is critical — otherwise the default sys.stderr sink at DEBUG level keeps emitting.

Structured logs to a JSON aggregator

python
logger.add(
    "logs/app.json",
    rotation="00:00",       # rotate at midnight
    retention="30 days",
    serialize=True,
    enqueue=True,
)

logger.bind(service="payments", env="prod").info("checkout completed",
                                                  user_id=42, amount_usd=99.50)

serialize=True emits one JSON object per line — each carries record.extra fields, the timestamp, level, source location, and any exception payload. Datadog, Loki, Splunk, and CloudWatch all consume this format natively.

Per-request context with contextualize

python
from contextlib import contextmanager
from uuid import uuid4
from loguru import logger

@contextmanager
def request_context(req):
    with logger.contextualize(request_id=str(uuid4()), path=req.path):
        yield

# in a request handler
with request_context(req):
    logger.info("started")
    process(req)
    logger.info("finished")

contextualize() is a context-local stack — fields added apply to every logger.* call within the block and are removed on exit. Thread-safe and async-safe. The request_id propagates through every log line for that request, making distributed tracing in logs cheap.

Bridging stdlib logging into loguru

python
import logging
from loguru import logger

class InterceptHandler(logging.Handler):
    def emit(self, record):
        try:
            level = logger.level(record.levelname).name
        except ValueError:
            level = record.levelno
        frame, depth = logging.currentframe(), 2
        while frame and frame.f_code.co_filename == logging.__file__:
            frame = frame.f_back
            depth += 1
        logger.opt(depth=depth, exception=record.exc_info).log(
            level, record.getMessage()
        )

logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)

This is the canonical recipe — third-party libraries logging via logging.getLogger(__name__) now flow through loguru's sinks. force=True overrides any prior logging.basicConfig (frameworks like Flask, Django, FastAPI set their own).

Exception capture decorator

python
@logger.catch(reraise=True)
def risky():
    raise ValueError("bad input")

risky()  # logs the full traceback + locals, then re-raises

@logger.catch wraps a function. With reraise=True (the default for callable use) it logs and re-raises; without, it swallows. Loguru's traceback formatter shows local variable values at each frame — strictly better than logging.exception().

Performance tuning

Loguru's defaults are tuned for correctness and ergonomics, not throughput. Levers:

  • enqueue=True moves the sink onto a background thread. Synchronous formatting (especially with serialize=True) becomes non-blocking for the caller. Trade-off: log lines can be lost on crash if the queue hasn't flushed. Acceptable for app logs; not for audit logs.
  • Format string cost. Loguru's format strings are richer than stdlib's — every {record.thread.id} etc. is a method call. For very high-volume logs (>10k/s) prefer minimal format strings or serialize=True (skips formatting entirely).
  • Level filtering at the sink. logger.add(..., level="WARNING") filters before formatting. Cheaper than a filter= callable.
  • lazy=True in logger.opt(lazy=True) defers argument evaluation until the level filter passes. Useful for expensive log args (logger.opt(lazy=True).debug("state: {}", lambda: dump_state())).
  • Sync sinks bypass the queue. enqueue=False (default) writes inline. Lower latency per call but blocks the caller during slow I/O (network sinks, slow disk).
  • compression= on rotation runs in a thread by default. Doesn't block log calls, but I/O scheduling can spike — schedule rotation at quiet times where possible.

For a service emitting 1M log lines/hour, enqueue=True + serialize=True + level="INFO" on a single file sink is the canonical baseline. Add stderr at WARNING for ops visibility.

Configuration & layout patterns

Loguru is intentionally config-free at install time — every application calls logger.add(...) to wire up sinks. Conventions that scale:

  • Centralise sink setup in one modulesrc/mypkg/logging_setup.py that the app entry point calls early. Don't sprinkle logger.add() calls around the codebase; debugging which sinks are active becomes impossible.
  • Always logger.remove() first. The default stderr sink at DEBUG level is rarely what you want in production. Remove and add explicitly.
  • Pin sink IDs as constants. STDERR_SINK_ID = logger.add(sys.stderr, ...). Later you can logger.remove(STDERR_SINK_ID) to quiet just that sink (e.g. during a noisy test).
  • Environment-driven config. Read LOG_LEVEL, LOG_FORMAT, LOG_FILE from env. Don't bake levels into source — operators need to bump verbosity without redeploying.
  • Separate sinks per concern. App logs at one level, audit logs at another, slow-query logs to their own file. The cost of an extra sink is minimal; the readability of segregated outputs is large.

For libraries that do want to expose a loguru handle, the convention is to give callers a logger = ... instance they can configure — but most libraries should log via stdlib and let the application choose.

Version migration guide

Loguru has stayed on 0.x since 2018 — the maintainer prefers signalling potential breakage. The 0.x → 0.y bumps occasionally tighten or shift APIs:

0.7.x (2023–2026)

  • Python 3.13 support added.
  • Type hints improved across the public API.
  • Minor changes to format= callable signatures; check release notes.

0.6.x (2022)

  • Improved async sink behaviour with enqueue=True.
  • New logger.complete() to await pending queued records.

0.5.x (2020–2021)

  • The serialize=True JSON format stabilised — fields added without breaking consumers.

Upgrade pattern:

  • Pin exact version (loguru==0.7.3) in pyproject.toml.
  • Bump deliberately and re-test custom format= callables — most upgrade breakage hits there.
  • The from loguru import logger import is stable; built-in handler types occasionally change keyword names.

A single-maintainer project with conservative semver — the project is widely production-used despite the 0.x prefix, but treat each bump as a code review.

Troubleshooting common errors

SymptomCauseFix
Log messages from third-party libraries don't appearThose libraries log via stdlib logging, not loguruInstall the InterceptHandler recipe (see Real-world recipes above).
caplog in pytest doesn't capture loguru outputcaplog hooks stdlib logging; loguru is a separate systemUse the loguru-pytest plugin's caplog shim, or temporarily add a logger.add(caplog.handler) sink in the fixture.
Logs disappear after logger.remove()remove() with no args removes all sinks including default stderrTrack the sink ID returned by add(); pass to remove(sink_id) to target one.
Format callable raises and kills logging silentlyBuggy format function causes loguru to swallow the errorRun with logger.add(..., catch=False) to surface format errors during development.
logger.add(file_path) doesn't create parent dirsLoguru passes the path to open() directlyPath(file_path).parent.mkdir(parents=True, exist_ok=True) before adding.
Multiple processes writing to one log fileDefault file sink is not multi-process safeAdd enqueue=True for thread safety; for multi-process, write to separate files or to a centralised aggregator.
Exception traceback is one linelogger.error("oops") doesn't capture exception contextUse logger.exception("oops") inside an except block, or @logger.catch.
Colors leak into log filescolorize=True writes ANSI codes regardless of sink typeSet colorize=False for file sinks; leave default behaviour for terminals (auto-detect).

logger.add(sys.stderr, format="{message}", catch=False) is the diagnostic config — minimal format, no swallow.

Ecosystem integrations

Loguru is parallel to most of the Python logging world rather than integrated with it:

  • logging (stdlib) — the InterceptHandler recipe bridges. Required for any project using libraries that log via logging.getLogger.
  • structlog — alternative structured logger. More processor-pipeline-oriented; less ergonomic for simple cases. Don't run both.
  • rich.logging.RichHandler — beautiful console output. Pair with stdlib logging (not loguru). Loguru's built-in colorisation covers most of the same ground.
  • Sentry / DataDog / Honeycomb SDKs — usually expect stdlib logging. The InterceptHandler routes loguru → stdlib → SDK; works correctly when the SDK's LoggingIntegration is configured to ignore the InterceptHandler frame.
  • pytestcaplog is stdlib-only. The loguru-pytest package adds a caplog-like fixture for loguru records.
  • uvicorn / gunicorn — these servers configure stdlib logging. Combine with InterceptHandler to unify app + server logs.
  • json-logging — superseded by serialize=True in loguru. Don't combine.

CI integration

In CI, loguru is usually a transitive dep — log output goes to stderr where the runner captures it. Two patterns worth considering:

  • --log-cli-level=INFO when paired with pytest. If using logger.add(sys.stderr, level="INFO") and the InterceptHandler, the bridge ensures all log lines appear inline with test output.
  • Log shipping from CIserialize=True to a file, then upload as a build artefact. Useful for post-failure forensic analysis of long-running integration tests.
yaml
- run: pytest --log-cli-level=INFO 2>&1 | tee test.log
- uses: actions/upload-artifact@v4
  if: failure()
  with:
    name: test-logs
    path: test.log

For services, the production deploy is where loguru config matters — CI just exercises the codepath.

When NOT to use this

Loguru is excellent for applications. Cases where it isn't the right tool:

  • Libraries published to PyPI for downstream consumers. Forcing loguru on every consumer is hostile. Libraries should log via logging.getLogger(__name__) and let the application choose the backend.
  • Strict-stdlib environments — Lambda layers, embedded Pythons, code that ships in air-gapped contexts. Stdlib logging is everywhere; loguru is one more dep to vendor.
  • Heavy structured-logging pipelines with custom processors. structlog is purpose-built for that; loguru's format= callable is less composable.
  • Extreme high-throughput logging (10M+ lines/hour). picologging (C extension) or direct file writes outperform.
  • Existing stdlib codebases. Migrating a 100k-line app to loguru rarely pays off. The InterceptHandler bridge captures loguru-via-stdlib equally well — there's no need to flip the import.

See also