Describe the bug
_deepcopy_with_exceptions in haystack/core/pipeline/utils.py rebuilds every dict as a plain dict:
if isinstance(obj, dict):
return {k: _deepcopy_with_exceptions(v) for k, v in obj.items()}
For list, tuple and set the function keeps the original type (type(obj)(...)), and #11981 added the same care for namedtuples. Dict subclasses do not get that treatment. A defaultdict, OrderedDict or Counter comes back as a plain dict, and a defaultdict loses its default_factory.
Pipeline.run copies its inputs with this function (pipeline.py and base.py call it on the input data), so a component receives a different type than the caller passed in.
Error message and reproduction
Direct call:
from collections import OrderedDict, defaultdict
from haystack.core.pipeline.utils import _deepcopy_with_exceptions as d
print(type(d(OrderedDict(a=1))).__name__) # dict
print(type(d(defaultdict(list, a=[1]))).__name__) # dict
d(defaultdict(list))["missing"] # KeyError: 'missing'
Through a pipeline:
from collections import defaultdict
from haystack import Pipeline, component
@component
class Lookup:
@component.output_types(v=int)
def run(self, d: dict):
return {"v": len(d["absent"])}
p = Pipeline()
p.add_component("d", Lookup())
p.run({"d": {"d": defaultdict(list)}})
# PipelineRuntimeError ... Error: 'absent'
Calling Lookup().run(d=defaultdict(list)) directly returns {"v": 0}. Only the pipeline run fails, because the input was converted to a plain dict on the way in.
I ran both snippets twice and got identical output each time.
Expected behavior
The copy keeps the type of a dict subclass, the same way it already does for lists, tuples, sets and namedtuples.
Possible fix
For dict subclasses, shallow copy the container so its type and state are kept, then fill it with copied values:
if isinstance(obj, dict):
if type(obj) is dict:
return {k: _deepcopy_with_exceptions(v) for k, v in obj.items()}
try:
new = copy.copy(obj)
new.clear()
for k, v in obj.items():
new[k] = _deepcopy_with_exceptions(v)
return new
except Exception:
return {k: _deepcopy_with_exceptions(v) for k, v in obj.items()}
I tried this on a local copy. OrderedDict, defaultdict and Counter all keep their type, compare equal to the original, and the defaultdict still creates missing keys. I did not run the full test suite. Given the current Haystack contribution state I am leaving this as an issue and am happy to send a PR if a maintainer wants one.
Additional context
Checked against main at 87dba65; haystack/core/pipeline/utils.py there is identical to the file I tested. Python 3.14 locally, Haystack from main. I searched open and closed issues and PRs for this function and for dict subclass copying and found only #11981 (namedtuples), which this is the sibling of.
Disclosure: I used an AI coding assistant to help find and write this up. I ran the reproductions myself and checked the output.
Describe the bug
_deepcopy_with_exceptionsinhaystack/core/pipeline/utils.pyrebuilds every dict as a plaindict:For
list,tupleandsetthe function keeps the original type (type(obj)(...)), and #11981 added the same care for namedtuples. Dict subclasses do not get that treatment. Adefaultdict,OrderedDictorCountercomes back as a plaindict, and adefaultdictloses itsdefault_factory.Pipeline.runcopies its inputs with this function (pipeline.pyandbase.pycall it on the input data), so a component receives a different type than the caller passed in.Error message and reproduction
Direct call:
Through a pipeline:
Calling
Lookup().run(d=defaultdict(list))directly returns{"v": 0}. Only the pipeline run fails, because the input was converted to a plain dict on the way in.I ran both snippets twice and got identical output each time.
Expected behavior
The copy keeps the type of a dict subclass, the same way it already does for lists, tuples, sets and namedtuples.
Possible fix
For dict subclasses, shallow copy the container so its type and state are kept, then fill it with copied values:
I tried this on a local copy.
OrderedDict,defaultdictandCounterall keep their type, compare equal to the original, and thedefaultdictstill creates missing keys. I did not run the full test suite. Given the current Haystack contribution state I am leaving this as an issue and am happy to send a PR if a maintainer wants one.Additional context
Checked against
mainat 87dba65;haystack/core/pipeline/utils.pythere is identical to the file I tested. Python 3.14 locally, Haystack from main. I searched open and closed issues and PRs for this function and for dict subclass copying and found only #11981 (namedtuples), which this is the sibling of.Disclosure: I used an AI coding assistant to help find and write this up. I ran the reproductions myself and checked the output.