Skip to content

SDResult.sd_updates is mutable despite frozen=True dataclass framing #757

@paddymul

Description

@paddymul

Summary

SDResult in buckaroo/jlisp/configure_utils.py is declared @dataclass(frozen=True), but sd_updates is a plain dict so callers retain a live reference and can mutate it after constructing the result.

@dataclass(frozen=True)
class SDResult:
    df: Any
    sd_updates: Dict[str, Dict[str, Any]]

frozen=True only blocks reassignment of the field itself. result.sd_updates['x'] = ... still mutates the same dict the caller holds — and the same dict the interpreter is about to read from in apply-result!.

Why it's not a correctness bug today

The interpreter's _apply_result closure (configure_utils.py:82) only reads from result.sd_updates. No code in the SDResult write path mutates after construction. So in practice nothing breaks.

But the docstring says:

Frozen because the result is a one-shot return value — the interpreter only reads from it.

That overstates what frozen=True guarantees, and a future change inside _apply_result (or a clever command author chaining results) could rely on the framing.

Options

Pick one:

  1. Drop the framing. Edit the docstring to say the dataclass is frozen to prevent field reassignment, not to make the contents immutable. Cheapest fix.
  2. Make it actually immutable. Convert sd_updates to a nested-immutable form in __post_init__ — wrap the outer dict in MappingProxyType, and either wrap each inner dict too or convert to frozenset of (col, key, value) tuples. The merge loop in _apply_result would need to handle the new shape.
  3. Validate shape on construction. Add a __post_init__ that asserts sd_updates is a dict of dicts. Doesn't fix mutability but catches the related class of bug where a command returns SDResult(df, "not a dict") — currently surfaces as a confusing TypeError deep in the lisp dispatch.

Option 1 is the minimal honest fix. Option 2 mirrors the read-only MappingProxyType treatment already applied to sd in buckaroo_transform, and would be consistent with the design doc's read-only-by-default stance.

Refs

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions