{"record":{"id":"3cbdf763e5286c9b","repo":"deepset-ai/haystack","slug":"could-not-find-attribute-part-in-container","errorCode":null,"errorMessage":"Could not find attribute '{part}' in {container}","messagePattern":"Could not find attribute '(.+?)' in (.+?)","errorType":"exception","errorClass":"DeserializationError","httpStatus":null,"severity":"error","filePath":"haystack/utils/callable_serialization.py","lineNumber":119,"sourceCode":"        try:\n            mod: Any = thread_safe_import(module_name)\n        except Exception:\n            # keep reducing i until we find a valid module import\n            continue\n\n        attr_value = mod\n        for part in parts[i:]:\n            # A handle legitimately walks `module.Class.method`, never into an object's internals.\n            # Refuse dunder/frame attributes (`__globals__`, `__dict__`, `__class__`, ...) before the\n            # getattr: `<func>.__globals__` yields a live module namespace (a gateway to the allowlist\n            # state and to `__builtins__`/`eval`) even though the traversal never leaves an allowlisted\n            # module, so neither the module allowlist nor the resolved-object checks below would catch it.\n            _check_traversable_attribute(part, callable_handle)\n            try:\n                attr_value = getattr(attr_value, part)\n            except AttributeError as e:\n                container = getattr(attr_value, \"__name__\", type(attr_value).__name__)\n                raise DeserializationError(f\"Could not find attribute '{part}' in {container}\") from e\n            # A crafted handle can walk through an object re-exported from an unallowlisted module and then reach a\n            # final callable whose own module is allowlisted. For example, an allowlisted Haystack module re-exports\n            # `rich.console.Console`; walking through that class to `Console._environ.update` ends at\n            # `collections.abc.MutableMapping.update`, hiding the unallowlisted `rich` hop from the final check below.\n            # Validate every object reached during traversal so no intermediate hop can escape the allowlist.\n            _check_resolved_module_allowed(attr_value, declared_module=module_name)\n\n        # when the attribute is a classmethod, we need the underlying function\n        if isinstance(attr_value, (classmethod, staticmethod)):\n            attr_value = attr_value.__func__\n\n        # Handle the case where @tool decorator replaced the function with a Tool object\n        if isinstance(attr_value, Tool):\n            attr_value = attr_value.function or attr_value.async_function\n\n        # Handle the case where @hook decorator replaced the function with a FunctionHook object\n        if isinstance(attr_value, FunctionHook):\n            attr_value = attr_value.function or attr_value.async_function","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/deepset-ai/haystack/blob/e318778c9bf60a1963e3b5f451359655dd696c30/haystack/utils/callable_serialization.py#L101-L137","documentation":"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.","triggerScenarios":"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.","commonSituations":"Library version upgrade renamed the function; pipeline YAML written with an older haystack version; typo in the stored callable path.","solutions":["Verify the dotted path exists in the installed version (python -c 'import ...' and getattr chain)","Update the serialized pipeline's callable reference to the new path after a rename","Pin the library version matching the serialized pipeline","Check for typos in module and attribute names in the stored handle"],"exampleFix":"// before\n\"splitting_function\": \"haystack.components.preprocessors.old_split\"\n// after\n\"splitting_function\": \"haystack.components.preprocessors.split\"  # updated to current path","handlingStrategy":"validation","validationCode":"import importlib\n\ndef handle_resolves(handle: str) -> bool:\n    parts = handle.split(\".\")\n    obj = importlib.import_module(parts[0])\n    for part in parts[1:]:\n        if not hasattr(obj, part):\n            return False\n        obj = getattr(obj, part)\n    return True","typeGuard":"import importlib\nfrom collections.abc import Callable\n\ndef resolves_to_callable(handle: str) -> bool:\n    try:\n        parts = handle.split(\".\")\n        obj = importlib.import_module(parts[0])\n        for part in parts[1:]:\n            obj = getattr(obj, part)\n    except (ImportError, AttributeError):\n        return False\n    return callable(obj)","tryCatchPattern":"try:\n    pipeline = Pipeline.loads(yaml_str)\nexcept DeserializationError as e:\n    logger.error(\"callable path unresolvable: %s\", e)\n    raise","preventionTips":["Check dotted paths still exist after upgrading haystack or custom packages","Pin library versions for stored pipeline files or migrate handles on upgrade","Validate callable handles in pipeline YAML in CI"],"tags":["python","deserialization","attributeerror","callable","haystack"],"backgroundTag":"attribute-not-found","analyzedSha":"e318778c9bf60a1963e3b5f451359655dd696c30","analyzedAt":"2026-08-30T11:45:20.711Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}