From 1f6779f543a0babb1a90aec96b80d5c0a81d2f8e Mon Sep 17 00:00:00 2001 From: Muhamed Fazal PS Date: Wed, 29 Jul 2026 22:03:23 +0530 Subject: [PATCH 1/3] Fix: prevent PositionalArgumentsFormatter from crashing on TypeError When PositionalArgumentsFormatter encounters a mismatch between the event string and the positional args (e.g. 'Info message' with args=('x',)), it raises an uncaught TypeError, crashing the application. This behavior differs from the Python stdlib logging module, which gracefully catches formatting errors to prevent logging from taking down the application. This patch wraps the formatting operation in a try/except block catching TypeError and ValueError. If formatting fails, the event string is left unformatted and the application continues running. Added a regression test to ensure this behavior is maintained. Closes #258 --- src/structlog/stdlib.py | 8 +++++++- tests/test_stdlib.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/structlog/stdlib.py b/src/structlog/stdlib.py index 2906598d..303157a2 100644 --- a/src/structlog/stdlib.py +++ b/src/structlog/stdlib.py @@ -802,7 +802,13 @@ def __call__( if len(args) == 1 and isinstance(args[0], dict) and args[0]: args = args[0] - event_dict["event"] %= args + try: + event_dict["event"] %= args + except (TypeError, ValueError): + # If formatting fails (e.g. mismatched %s placeholders), + # gracefully leave the event unformatted instead of crashing + # the application. This mimics the stdlib logging behavior. + pass if self.remove_positional_args and args is not None: del event_dict["positional_args"] diff --git a/tests/test_stdlib.py b/tests/test_stdlib.py index 655cadb4..c491767c 100644 --- a/tests/test_stdlib.py +++ b/tests/test_stdlib.py @@ -519,6 +519,29 @@ def test_args_removed_if_empty(self): assert {} == formatter(None, None, {"positional_args": ()}) + + def test_graceful_failure_on_formatting_error(self): + """ + If positional arguments do not match the formatting placeholders + in the event string, PositionalArgumentsFormatter should not crash + with a TypeError or ValueError. It should leave the event string + unformatted. + + Regression test for https://github.com/hynek/structlog/issues/258. + """ + formatter = PositionalArgumentsFormatter() + + # Mismatched placeholders (event has no %s but args are passed) + event_dict = formatter( + None, + None, + {"event": "Info message", "positional_args": ("x",)}, + ) + + assert "Info message" == event_dict["event"] + assert "positional_args" not in event_dict + + class TestAddLogLevelNumber: @pytest.mark.parametrize(("level", "number"), NAME_TO_LEVEL.items()) def test_log_level_number_added(self, level, number): From adbc4630c9a19cd619b4fa4f4eb00b0786dba4b4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:34:16 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_stdlib.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_stdlib.py b/tests/test_stdlib.py index c491767c..6bd054fa 100644 --- a/tests/test_stdlib.py +++ b/tests/test_stdlib.py @@ -518,8 +518,6 @@ def test_args_removed_if_empty(self): assert {} == formatter(None, None, {"positional_args": ()}) - - def test_graceful_failure_on_formatting_error(self): """ If positional arguments do not match the formatting placeholders @@ -530,7 +528,7 @@ def test_graceful_failure_on_formatting_error(self): Regression test for https://github.com/hynek/structlog/issues/258. """ formatter = PositionalArgumentsFormatter() - + # Mismatched placeholders (event has no %s but args are passed) event_dict = formatter( None, From 974bcd22b8d2a3e00f4a51e5b5344c0f81a3b67d Mon Sep 17 00:00:00 2001 From: Muhamed Fazal PS Date: Wed, 29 Jul 2026 23:28:31 +0530 Subject: [PATCH 3/3] fix: use contextlib.suppress instead of try-except-pass for SIM105 --- src/structlog/stdlib.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/structlog/stdlib.py b/src/structlog/stdlib.py index 303157a2..827137b4 100644 --- a/src/structlog/stdlib.py +++ b/src/structlog/stdlib.py @@ -12,6 +12,7 @@ from __future__ import annotations import asyncio +import contextlib import contextvars import functools import logging @@ -802,13 +803,8 @@ def __call__( if len(args) == 1 and isinstance(args[0], dict) and args[0]: args = args[0] - try: + with contextlib.suppress(TypeError, ValueError): event_dict["event"] %= args - except (TypeError, ValueError): - # If formatting fails (e.g. mismatched %s placeholders), - # gracefully leave the event unformatted instead of crashing - # the application. This mimics the stdlib logging behavior. - pass if self.remove_positional_args and args is not None: del event_dict["positional_args"]