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
pip install loguru
Output: (none — exits 0 on success). Pure-Python, no compiled deps.
uv add loguru
Output: dependency added to pyproject.toml
poetry add loguru
Output: updated lockfile + virtualenv install
Versioning & Python support
- Current stable line is
0.7.x. The project has stayed on0.xsince 2018 — the maintainer prefers0.xto 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.xseries. - 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 supportwin32-setctime(on Windows) — file timestamp control for log rotationaiocontextvars(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 stdlibgzip/zipfile/tarfile, no extra needed. - JSON serialisation —
logger.add(..., serialize=True)works out of the box. - Async support —
logger.add(..., enqueue=True)makes the sink non-blocking; the library is async-safe without an[async]extra.
Alternatives
| Package | Trade-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. |
structlog | Structured-first (JSON / key-value). Composable processors pipeline. Use when log aggregation (Datadog, ELK) is the primary consumer. |
picologging | C-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. |
eliot | Action-tree logging — every log is part of a causal action. Different mental model; niche. |
Common gotchas
- Loguru is not a stdlib
loggingreplacement. Libraries you depend on still log vialogging.getLogger(...). Those messages will not flow through loguru's sinks unless you install theInterceptHandlerrecipe (a ~15-line snippet from the docs). - Per-record formatting is dynamic. Unlike stdlib logging's static
Formatter, loguru letsformat=be a callable. This is powerful but means a buggy formatter can crash every log call — keep it side-effect-free. - Single global
loggeris by design. There's nogetLogger(__name__)equivalent. Uselogger.bind(name=__name__)to attach module context if you need filtering by source. - 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. Withoutenqueue=True, two threads writing to the same file sink race on the underlying file handle. logger.add()returns a sink ID, not a handler object. To remove a sink, pass the ID tologger.remove(sink_id). Callinglogger.remove()with no arguments removes all sinks — including the default stderr one, leaving a silent logger.- Exception capture (
@logger.catch) re-raises by default. It logs and then propagates. Passreraise=Falseto swallow — but think hard before doing so; swallowed exceptions are why bugs ship. - 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
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
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
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
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
@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=Truemoves the sink onto a background thread. Synchronous formatting (especially withserialize=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 orserialize=True(skips formatting entirely). - Level filtering at the sink.
logger.add(..., level="WARNING")filters before formatting. Cheaper than afilter=callable. lazy=Trueinlogger.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 module —
src/mypkg/logging_setup.pythat the app entry point calls early. Don't sprinklelogger.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 canlogger.remove(STDERR_SINK_ID)to quiet just that sink (e.g. during a noisy test). - Environment-driven config. Read
LOG_LEVEL,LOG_FORMAT,LOG_FILEfrom 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=TrueJSON format stabilised — fields added without breaking consumers.
Upgrade pattern:
- Pin exact version (
loguru==0.7.3) inpyproject.toml. - Bump deliberately and re-test custom
format=callables — most upgrade breakage hits there. - The
from loguru import loggerimport 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
| Symptom | Cause | Fix |
|---|---|---|
| Log messages from third-party libraries don't appear | Those libraries log via stdlib logging, not loguru | Install the InterceptHandler recipe (see Real-world recipes above). |
caplog in pytest doesn't capture loguru output | caplog hooks stdlib logging; loguru is a separate system | Use 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 stderr | Track the sink ID returned by add(); pass to remove(sink_id) to target one. |
| Format callable raises and kills logging silently | Buggy format function causes loguru to swallow the error | Run with logger.add(..., catch=False) to surface format errors during development. |
logger.add(file_path) doesn't create parent dirs | Loguru passes the path to open() directly | Path(file_path).parent.mkdir(parents=True, exist_ok=True) before adding. |
| Multiple processes writing to one log file | Default file sink is not multi-process safe | Add enqueue=True for thread safety; for multi-process, write to separate files or to a centralised aggregator. |
| Exception traceback is one line | logger.error("oops") doesn't capture exception context | Use logger.exception("oops") inside an except block, or @logger.catch. |
| Colors leak into log files | colorize=True writes ANSI codes regardless of sink type | Set 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 vialogging.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 stdliblogging(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'sLoggingIntegrationis configured to ignore the InterceptHandler frame. pytest—caplogis stdlib-only. Theloguru-pytestpackage adds acaplog-like fixture for loguru records.uvicorn/gunicorn— these servers configure stdlib logging. Combine with InterceptHandler to unify app + server logs.json-logging— superseded byserialize=Truein 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=INFOwhen paired withpytest. If usinglogger.add(sys.stderr, level="INFO")and the InterceptHandler, the bridge ensures all log lines appear inline with test output.- Log shipping from CI —
serialize=Trueto a file, then upload as a build artefact. Useful for post-failure forensic analysis of long-running integration tests.
- 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
loggingis everywhere; loguru is one more dep to vendor. - Heavy structured-logging pipelines with custom processors.
structlogis purpose-built for that; loguru'sformat=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
- Python: loguru — sinks, rotation, retention,
InterceptHandlerrecipe - Concept: Filesystem — rotation, retention, and on-disk semantics
- Packages: pip-pytest —
caplogworks against stdlib logging, not loguru directly