deepset-ai/haystack · error · DeserializationError

The final attribute is not callable: {attr_value}

Error message

The final attribute is not callable: {attr_value}

What it means

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.

Source

Thrown at haystack/utils/callable_serialization.py:140

            # `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

        if not callable(attr_value):
            raise DeserializationError(f"The final attribute is not callable: {attr_value}")

        # Final defense: gate on the module the resolved callable actually comes from, not on the
        # declared handle. This catches a dangerous callable bound as a plain (non-module) attribute
        # of an allowlisted object, which the module-walk check above would not see. `module_name`
        # is the allowlisted module we resolved from, so a private C accelerator backing it (e.g.
        # `operator.add` -> `_operator`) is still accepted.
        _check_resolved_module_allowed(attr_value, declared_module=module_name)

        # `builtins` is on the allowlist (for `builtins.print` etc.), so the module check
        # above does not stop dangerous builtins like `eval`/`exec` from resolving here. Block them.
        _check_not_denied_builtin(attr_value, callable_handle)

        # The module check also does not stop import primitives that live inside an allowlisted
        # namespace (e.g. `haystack...thread_safe_import`), which are gateways to code execution
        # equivalent to the denied builtin `__import__`. Block them too.
        _check_not_denied_callable(attr_value, callable_handle)

        # Refuse the deserializer's own machinery — the allowlist-administration function

View on GitHub (pinned to e318778c9b)

Solutions

  1. Point the handle at the actual function/callable (e.g. 'mymodule.my_func')
  2. Verify with callable(getattr(module, name)) before loading the pipeline
  3. Fix refactoring that replaced a function with a value at the same path
  4. If using @hook-decorated members, ensure the underlying function/async_function is set

Example fix

// before
"splitting_function": "mymodule.SPLIT_SIZE"   # constant, not callable
// after
"splitting_function": "mymodule.split_text"   # actual function
Defensive patterns

Strategy: validation

Validate before calling

import importlib

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

Type guard

from collections.abc import Callable

def assert_callable(obj) -> bool:
    return callable(obj)

Try / catch

try:
    pipeline = Pipeline.loads(yaml_str)
except DeserializationError as e:
    if "not callable" in str(e):
        logger.error("handle points to a non-callable attribute")
    raise

Prevention

When it happens

Trigger: Serialized handle like 'mymodule.SOME_CONSTANT' passed to from_dict/deserialize_callable where the attribute exists but isn't a function.

Common situations: 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.

Related errors


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