Skip to content

fix: preserve dict subclass types in _deepcopy_with_exceptions - #13094

Open
inchang-ing wants to merge 1 commit into
deepset-ai:mainfrom
inchang-ing:fix-deepcopy-dict-subclass
Open

inchang-ing wants to merge 1 commit into
deepset-ai:mainfrom
inchang-ing:fix-deepcopy-dict-subclass

Conversation

@inchang-ing

Copy link
Copy Markdown

Related Issues

Proposed Changes:

_deepcopy_with_exceptions rebuilt every dict with a dict comprehension, so any dict subclass came back as a plain dict:

if isinstance(obj, dict):
    return {k: _deepcopy_with_exceptions(v) for k, v in obj.items()}

A defaultdict lost its default_factory, an OrderedDict lost its ordering guarantee, and a Counter was demoted to dict. The list/tuple/set branch right above already preserves the concrete type via type(obj)(...), and the namedtuple special case (see the release note fix-deepcopy-namedtuple-*) does the same, so dict subclasses were the remaining gap. Because Pipeline.run copies its inputs with this helper, a component could receive a different type than the caller passed in.

The dict branch now rebuilds the container as its own type instead of a plain dict:

if isinstance(obj, dict):
    result = copy(obj)
    result.clear()
    result.update((k, _deepcopy_with_exceptions(v)) for k, v in obj.items())
    return result

A plain copy is used rather than type(obj)(<iterable>) because dict subclasses have incompatible constructors (defaultdict takes default_factory as its first positional argument), so a generic iterable-based rebuild is not safe. Copying the (empty) container preserves the subclass and all of its configuration — including defaultdict.default_factory — and refilling it with deep-copied values keeps the deep-copy semantics identical to the plain-dict path.

How did you test it?

Added TestDeepcopyWithFallback cases in test/core/pipeline/test_utils.py:

  • a parametrized case over defaultdict, OrderedDict and Counter asserting the type is preserved and the values are deep-copied;
  • a case asserting a defaultdict keeps its default_factory (a missing key still triggers the factory instead of raising KeyError).

Verified the tests fail on main (the type assertion sees dict instead of the subclass) and pass with the change.

Notes for the reviewer

  • copy(obj) on a plain dict returns a plain dict, so existing behavior for the common case is unchanged.
  • Contributes a release note (releasenotes/notes/fix-deepcopy-dict-subclass-*.yaml), matching the earlier namedtuple fix.

Checklist


This PR was fully generated with an AI assistant. I have reviewed the changes and run the relevant tests.

The dict branch rebuilt every dict with a dict comprehension, so
`defaultdict`, `OrderedDict` and `Counter` values passed to a pipeline
came back as plain `dict`. `Pipeline.run` copies its inputs with this
helper, so a component received a different type than the caller passed
in, and a `defaultdict` lost its `default_factory` in the process.

Rebuild the container as its own type (copy, clear and refill with
deep-copied values) instead of collapsing it to a plain `dict`, matching
the existing `type(obj)` handling for lists/tuples/sets and namedtuples.
@inchang-ing
inchang-ing requested a review from a team as a code owner October 3, 2026 10:42
@inchang-ing
inchang-ing requested review from anakin87 and removed request for a team October 3, 2026 10:42
@vercel

vercel Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

@inchang-ing is attempting to deploy a commit to the deepset Team on Vercel.

A member of the Team first needs to authorize it.

@CLAassistant

CLAassistant commented Oct 3, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@inchang-ing

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_deepcopy_with_exceptions turns dict subclasses (defaultdict, OrderedDict, Counter) into plain dict, so pipeline inputs lose their type

2 participants