{"record":{"id":"a38e8a89ad5b0cef","repo":"deepset-ai/haystack","slug":"the-final-attribute-is-not-callable-attr-value","errorCode":null,"errorMessage":"The final attribute is not callable: {attr_value}","messagePattern":"The final attribute is not callable: (.+?)","errorType":"exception","errorClass":"DeserializationError","httpStatus":null,"severity":"error","filePath":"haystack/utils/callable_serialization.py","lineNumber":140,"sourceCode":"            # `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\n\n        if not callable(attr_value):\n            raise DeserializationError(f\"The final attribute is not callable: {attr_value}\")\n\n        # Final defense: gate on the module the resolved callable actually comes from, not on the\n        # declared handle. This catches a dangerous callable bound as a plain (non-module) attribute\n        # of an allowlisted object, which the module-walk check above would not see. `module_name`\n        # is the allowlisted module we resolved from, so a private C accelerator backing it (e.g.\n        # `operator.add` -> `_operator`) is still accepted.\n        _check_resolved_module_allowed(attr_value, declared_module=module_name)\n\n        # `builtins` is on the allowlist (for `builtins.print` etc.), so the module check\n        # above does not stop dangerous builtins like `eval`/`exec` from resolving here. Block them.\n        _check_not_denied_builtin(attr_value, callable_handle)\n\n        # The module check also does not stop import primitives that live inside an allowlisted\n        # namespace (e.g. `haystack...thread_safe_import`), which are gateways to code execution\n        # equivalent to the denied builtin `__import__`. Block them too.\n        _check_not_denied_callable(attr_value, callable_handle)\n\n        # Refuse the deserializer's own machinery — the allowlist-administration function","sourceCodeStart":122,"sourceCodeEnd":158,"githubUrl":"https://github.com/deepset-ai/haystack/blob/e318778c9bf60a1963e3b5f451359655dd696c30/haystack/utils/callable_serialization.py#L122-L158","documentation":"deserialize_callable resolved the dotted path successfully but the final object is not callable, so it raises DeserializationError. The stored handle points to a module attribute such as a constant, class attribute, or data member rather than a function or callable class.","triggerScenarios":"Serialized handle like 'mymodule.SOME_CONSTANT' passed to from_dict/deserialize_callable where the attribute exists but isn't a function.","commonSituations":"Hand-edited pipeline YAML referencing a config constant instead of a function; attribute shadowed after refactoring so the path now resolves to a non-callable; FunctionHook whose function was None.","solutions":["Point the handle at the actual function/callable (e.g. 'mymodule.my_func')","Verify with callable(getattr(module, name)) before loading the pipeline","Fix refactoring that replaced a function with a value at the same path","If using @hook-decorated members, ensure the underlying function/async_function is set"],"exampleFix":"// before\n\"splitting_function\": \"mymodule.SPLIT_SIZE\"   # constant, not callable\n// after\n\"splitting_function\": \"mymodule.split_text\"   # actual function","handlingStrategy":"validation","validationCode":"import importlib\n\ndef is_callable_handle(handle: str) -> bool:\n    parts = handle.split(\".\")\n    obj = importlib.import_module(parts[0])\n    for part in parts[1:]:\n        obj = getattr(obj, part)\n    return callable(obj)","typeGuard":"from collections.abc import Callable\n\ndef assert_callable(obj) -> bool:\n    return callable(obj)","tryCatchPattern":"try:\n    pipeline = Pipeline.loads(yaml_str)\nexcept DeserializationError as e:\n    if \"not callable\" in str(e):\n        logger.error(\"handle points to a non-callable attribute\")\n    raise","preventionTips":["Reference functions, not constants or class attributes, in serialized handles","After refactors, grep pipeline YAML for renamed attribute paths","Validate handles with callable(getattr(...)) before loading"],"tags":["python","deserialization","callable","haystack"],"backgroundTag":"not-callable","analyzedSha":"e318778c9bf60a1963e3b5f451359655dd696c30","analyzedAt":"2026-08-30T11:45:20.711Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}