Skip to content

Plainlog vs Python Standard Library Logging

Aspect plainlog stdlib logging
Architecture Logger and Core are cleanly separated via a thread-safe SimpleQueue. The logger runs in the application thread; the Core runs in a dedicated daemon thread. Logger, Handler, and Formatter all run in the caller's thread. No built-in queue-based decoupling.
Blocking behavior Non-blocking by design. The logger merely builds a dict record and enqueues it; all I/O and formatting happen in the Core's background thread. Synchronous and blocking in the hot path (I/O inside emit() blocks the caller).
Record type Log records are plain Python dict objects. Log records are LogRecord instances (a class with many attributes).
Logger hierarchy No logger hierarchy. Multiple loggers share one singleton Core. Logger names are flat/dotted strings but not hierarchical beyond that. Strict hierarchical logger tree with parent-child propagation.
Configuration Configuration is done solely on the Core (handlers, processors, level). Uses named profiles (e.g. "default", "develop", "json", "file"). Environment variables PLAINLOG_PROFILE and PLAINLOG_LEVEL control initial setup. Configuration is done on loggers (propagation, level, filters) and handlers separately. Typically requires multiple lines of boilerplate.
Processors A list of callables (processors) run in order in the Core thread after dequeueing. Formatters and handlers are themselves processors. This is a first-class concept. No direct equivalent. Filters exist but are less composable.
Speed Logger hot path is minimal: build a dict, check level, enqueue to queue. Much faster than stdlib on the hot path. Hot path does level comparison, formatting, filter checks, and handler emission all inline.
Async support Same sync interface works in async code because logging is always non-blocking (the queue-based Core handles I/O). Requires QueueHandler/QueueListener setup for similar behavior, or dedicated async handlers with different method names.
bind / unbind / context Offers logger.bind(**kwargs) (returns a new logger with extra fields), logger.unbind(*args), logger.context(**kwargs) (context var), and logger.contextualize(**kwargs) context manager. No built-in equivalent; must use LoggerAdapter or extra dict manually.
Log levels Reuses stdlib level numbers and names (DEBUG, INFO, WARNING, ERROR, CRITICAL, NOTSET). Does not invent its own. The original source of these levels.
Profiles Ships with 12 named configuration profiles (default, develop, develop_no_color, cloud, json, file, fingerscrossed, fingerscrossed_file, empty, no_init, etc.) for one-liner setup. No profiles concept.
Dependencies Zero external dependencies. Part of the Python standard library.
Setup from plainlog import logger — ready to use immediately. import logging; logging.basicConfig(...); logger = logging.getLogger(...) — requires boilerplate.

Design Philosophy

plainlog is simple, small, fast, and non-blocking by default. It borrows best ideas from structlog (processor pipelines, dev console rendering, bind/unbind), loguru (logger/core separation with queue), logbook (FingersCrossed), and Twisted (record-as-dict philosophy).