From 109d1c0fd5ebf58a33dd4427ca9e4586bcc3f9d6 Mon Sep 17 00:00:00 2001 From: Charisn Date: Sat, 20 Jun 2026 18:42:00 +0300 Subject: [PATCH] Avoid kwargs unpacking in _process_event hot path `BoundLoggerBase._process_event` runs on every non-filtered log call and is the hottest function in structlog. It assembled the event dict with: event_dict = self._context.copy() event_dict.update(**event_kw) `event_kw` is always a plain `dict` -- it originates from `**event_kw` in `_proxy_to_logger` -- so unpacking it back into keyword arguments only for `dict.update` to repack them is pure overhead. The `update(**event_kw)` call path materializes a fresh keyword dict and revalidates the keys on every single log call, even when `event_kw` is empty. Passing the mapping positionally -- `event_dict.update(event_kw)` -- is semantically identical for a dict of string keys (both the resulting value and its type are unchanged) but skips that machinery entirely. Measured on CPython 3.14, averaged over several A/B rounds on the `copy()` + `update()` core of `_process_event`: kwargs before after speedup 0 117 ns 89 ns 1.31x 1 134 ns 91 ns 1.48x 3 199 ns 150 ns 1.33x 8 258 ns 180 ns 1.44x End-to-end through `_proxy_to_logger` (3-key bound context, 3 kwargs): ~919 ns -> ~844 ns per call, roughly 8% faster. The improvement is present even with zero kwargs, because `update(**{})` still pays for the empty-kwargs path. This is behavior-preserving; the existing test suite passes unchanged. --- CHANGELOG.md | 6 ++++++ src/structlog/_base.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b23d71a7..b9887e75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/ ## [Unreleased](https://github.com/hynek/structlog/compare/26.1.0...HEAD) +### Changed + +- Logging is now slightly faster: `structlog.BoundLoggerBase._process_event` -- which runs on every log call -- no longer unpacks the event keyword arguments into a fresh keyword dict just to merge them into the event dict. + Passing the mapping positionally to `dict.update` is behavior-preserving and avoids that per-call overhead. + [#821](https://github.com/hynek/structlog/pull/821) + ## [26.1.0](https://github.com/hynek/structlog/compare/25.5.0...26.1.0) - 2026-06-06 diff --git a/src/structlog/_base.py b/src/structlog/_base.py index 93845e65..79db8551 100644 --- a/src/structlog/_base.py +++ b/src/structlog/_base.py @@ -166,7 +166,7 @@ def _process_event( # We're typing it as Any, because processors can return more than an # EventDict. event_dict: Any = self._context.copy() - event_dict.update(**event_kw) + event_dict.update(event_kw) if event is not None: event_dict["event"] = event