Skip to content

fix(serializer): handle single-string __slots__ and detect cycles in slotted objects #2014

Open

Description

@passionworkeer

Describe the bug

In langfuse._utils.serializer.EventSerializer, slotted objects are handled by iterating over obj.__slots__:

if hasattr(obj, "__slots__"):
    return {
        slot: self.default(getattr(obj, slot, None))
        for slot in obj.__slots__
    }

Two distinct defects exist in this logic:

  1. Single string __slots__ attribute loss:
    According to the Python Language Reference (Data Model §3.3.2.4: __slots__), __slots__ can be defined as a single string naming a single attribute (e.g. __slots__ = "query"). When __slots__ is a string, iterating directly over it yields individual characters (q, u, e, r, y). getattr(obj, "q", None) returns None, resulting in {"q": null, "u": null, ...} while the actual attribute query is lost.

  2. Missing circular reference detection:
    Unlike the __dict__ branch which tracks obj_id in self.seen to immediately break reference cycles, the __slots__ branch lacks self.seen protection. When slotted objects contain reference cycles, each branch expands down to _MAX_DEPTH = 20, causing redundant recursion and bloated payloads.

To Reproduce

import json
from langfuse._utils.serializer import EventSerializer

# Case 1: Single string slot
class SingleSlot:
    __slots__ = "query"
    def __init__(self):
        self.query = "SELECT 1"

res = json.loads(EventSerializer().encode(SingleSlot()))
print(res)
# Current output: {"q": null, "u": null, "e": null, "r": null, "y": null}
# Expected: {"query": "SELECT 1"}

# Case 2: Self-referential slotted node
class SlotNode:
    __slots__ = ("next",)

node = SlotNode()
node.next = node
res = json.loads(EventSerializer().encode(node))
# Current output: recurses 20 layers deep
# Expected: {"next": "SlotNode"}

Proposed Fix

  1. Normalize string __slots__ to a 1-tuple (obj.__slots__,) if isinstance(obj.__slots__, str).
  2. Add self.seen tracking with try...finally to __slots__ (matching __dict__).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions