deepset-ai/haystack · error · DeserializationError

Could not find attribute '{part}' in {container}

Error message

Could not find attribute '{part}' in {container}

What it means

deserialize_callable walks the dotted path of a serialized callable and raised DeserializationError because an intermediate attribute 'part' could not be found via getattr on the current container. The serialized handle points to an attribute path that no longer exists on the target module/object.

Source

Thrown at haystack/utils/callable_serialization.py:119

        try:
            mod: Any = thread_safe_import(module_name)
        except Exception:
            # keep reducing i until we find a valid module import
            continue

        attr_value = mod
        for part in parts[i:]:
            # A handle legitimately walks `module.Class.method`, never into an object's internals.
            # Refuse dunder/frame attributes (`__globals__`, `__dict__`, `__class__`, ...) before the
            # getattr: `<func>.__globals__` yields a live module namespace (a gateway to the allowlist
            # state and to `__builtins__`/`eval`) even though the traversal never leaves an allowlisted
            # module, so neither the module allowlist nor the resolved-object checks below would catch it.
            _check_traversable_attribute(part, callable_handle)
            try:
                attr_value = getattr(attr_value, part)
            except AttributeError as e:
                container = getattr(attr_value, "__name__", type(attr_value).__name__)
                raise DeserializationError(f"Could not find attribute '{part}' in {container}") from e
            # A crafted handle can walk through an object re-exported from an unallowlisted module and then reach a
            # final callable whose own module is allowlisted. For example, an allowlisted Haystack module re-exports
            # `rich.console.Console`; walking through that class to `Console._environ.update` ends at
            # `collections.abc.MutableMapping.update`, hiding the unallowlisted `rich` hop from the final check below.
            # Validate every object reached during traversal so no intermediate hop can escape the allowlist.
            _check_resolved_module_allowed(attr_value, declared_module=module_name)

        # when the attribute is a classmethod, we need the underlying function
        if isinstance(attr_value, (classmethod, staticmethod)):
            attr_value = attr_value.__func__

        # Handle the case where @tool decorator replaced the function with a Tool object
        if isinstance(attr_value, Tool):
            attr_value = attr_value.function or attr_value.async_function

        # Handle the case where @hook decorator replaced the function with a FunctionHook object
        if isinstance(attr_value, FunctionHook):
            attr_value = attr_value.function or attr_value.async_function

View on GitHub (pinned to e318778c9b)

Solutions

  1. Verify the dotted path exists in the installed version (python -c 'import ...' and getattr chain)
  2. Update the serialized pipeline's callable reference to the new path after a rename
  3. Pin the library version matching the serialized pipeline
  4. Check for typos in module and attribute names in the stored handle

Example fix

// before
"splitting_function": "haystack.components.preprocessors.old_split"
// after
"splitting_function": "haystack.components.preprocessors.split"  # updated to current path
Defensive patterns

Strategy: validation

Validate before calling

import importlib

def handle_resolves(handle: str) -> bool:
    parts = handle.split(".")
    obj = importlib.import_module(parts[0])
    for part in parts[1:]:
        if not hasattr(obj, part):
            return False
        obj = getattr(obj, part)
    return True

Type guard

import importlib
from collections.abc import Callable

def resolves_to_callable(handle: str) -> bool:
    try:
        parts = handle.split(".")
        obj = importlib.import_module(parts[0])
        for part in parts[1:]:
            obj = getattr(obj, part)
    except (ImportError, AttributeError):
        return False
    return callable(obj)

Try / catch

try:
    pipeline = Pipeline.loads(yaml_str)
except DeserializationError as e:
    logger.error("callable path unresolvable: %s", e)
    raise

Prevention

When it happens

Trigger: Calling deserialize_callable or from_dict with a stored callable path like 'mypkg.utils.func' where 'utils' or 'func' was renamed/removed, or where the string was malformed.

Common situations: Library version upgrade renamed the function; pipeline YAML written with an older haystack version; typo in the stored callable path.

Related errors


AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30). Data as JSON: /api/errors/3cbdf763e5286c9b. Report an issue: GitHub.