Labels
bug, python-antipattern, config, rate-limiting
Summary
Two methods in the Config class use mutable default arguments (dict and list), which are evaluated only once at function definition time in Python.
If these objects are ever mutated in-place, the mutation persists across subsequent calls that rely on the default value. This can unintentionally leak state between independent sessions.
Python documentation:
https://docs.python.org/3/faq/programming.html#why-are-default-values-shared-between-objects
Affected Locations
File:
src/util/config_yml/__init__.py
1️⃣ get_messages() (line ~40)
def get_messages(
self,
user_id: str | None = None,
event: TriggerEvent | None = None,
after_messages: int | None = None,
last_messages: dict[str, str] = {}, # mutable default
) -> dict[str, str]:
2️⃣ get_message_rate_usage_limited() (line ~57)
def get_message_rate_usage_limited(
self,
user_id: str | None = None,
message_times_queue: list[str] = [], # mutable default
) -> MessageRate | None:
Current Behavior
Python creates the default {} and [] once at function definition time.
If the function mutates these objects, the mutation persists across future calls.
Example:
def example(data=[]):
data.append("x")
return data
example() # ['x']
example() # ['x', 'x']
example() # ['x', 'x', 'x']
Why This Matters
1️⃣ Static messaging (get_messages)
The last_messages dictionary is used to filter messages per user.
A shared default dictionary could cause one user’s message history to affect another user’s filtering logic.
2️⃣ Rate limiting (get_message_rate_usage_limited)
message_times_queue tracks timestamps used for rate limiting.
A shared list could accumulate timestamps across users, potentially causing:
- premature throttling
- inaccurate rate-limit enforcement
Current Impact
Based on current usage:
src/util/chainlit_helpers.py
(lines ~98 and ~137)
Both callers currently pass explicit arguments, meaning the issue is latent and not triggered today.
However, any future call relying on the default value could introduce subtle cross-session state leakage.
Static Analysis / Linter Warnings
This pattern is flagged by several Python linters:
- pylint →
W0102: dangerous-default-value
- flake8-bugbear →
B006
- ruff →
B006
Proposed Fix
Use None as the default and instantiate a fresh object inside the function.
Fix 1 — get_messages
def get_messages(
self,
user_id: str | None = None,
event: TriggerEvent | None = None,
after_messages: int | None = None,
- last_messages: dict[str, str] = {},
+ last_messages: dict[str, str] | None = None,
) -> dict[str, str]:
+ if last_messages is None:
+ last_messages = {}
Fix 2 — get_message_rate_usage_limited
def get_message_rate_usage_limited(
self,
user_id: str | None = None,
- message_times_queue: list[str] = [],
+ message_times_queue: list[str] | None = None,
) -> MessageRate | None:
+ if message_times_queue is None:
+ message_times_queue = []
Compatibility
This change is fully backward compatible, since:
- callers can still pass explicit
dict / list
- only the internal default initialization changes
Files to Update
src/util/config_yml/__init__.py
Labels
bug,python-antipattern,config,rate-limitingSummary
Two methods in the
Configclass use mutable default arguments (dictandlist), which are evaluated only once at function definition time in Python.If these objects are ever mutated in-place, the mutation persists across subsequent calls that rely on the default value. This can unintentionally leak state between independent sessions.
Python documentation:
https://docs.python.org/3/faq/programming.html#why-are-default-values-shared-between-objects
Affected Locations
File:
1️⃣
get_messages()(line ~40)2️⃣
get_message_rate_usage_limited()(line ~57)Current Behavior
Python creates the default
{}and[]once at function definition time.If the function mutates these objects, the mutation persists across future calls.
Example:
Why This Matters
1️⃣ Static messaging (
get_messages)The
last_messagesdictionary is used to filter messages per user.A shared default dictionary could cause one user’s message history to affect another user’s filtering logic.
2️⃣ Rate limiting (
get_message_rate_usage_limited)message_times_queuetracks timestamps used for rate limiting.A shared list could accumulate timestamps across users, potentially causing:
Current Impact
Based on current usage:
(lines ~98 and ~137)
Both callers currently pass explicit arguments, meaning the issue is latent and not triggered today.
However, any future call relying on the default value could introduce subtle cross-session state leakage.
Static Analysis / Linter Warnings
This pattern is flagged by several Python linters:
W0102: dangerous-default-valueB006B006Proposed Fix
Use
Noneas the default and instantiate a fresh object inside the function.Fix 1 —
get_messagesdef get_messages( self, user_id: str | None = None, event: TriggerEvent | None = None, after_messages: int | None = None, - last_messages: dict[str, str] = {}, + last_messages: dict[str, str] | None = None, ) -> dict[str, str]: + if last_messages is None: + last_messages = {}Fix 2 —
get_message_rate_usage_limiteddef get_message_rate_usage_limited( self, user_id: str | None = None, - message_times_queue: list[str] = [], + message_times_queue: list[str] | None = None, ) -> MessageRate | None: + if message_times_queue is None: + message_times_queue = []Compatibility
This change is fully backward compatible, since:
dict/listFiles to Update