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 functionView on GitHub (pinned to e318778c9b)
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
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
- 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
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
- Could not find attribute '{part}' in {container}
- Refusing to deserialize an OutputAdapter with unsafe=True wh
- Refusing to deserialize an OutputAdapter with custom filters
- Missing 'type' in serialization data
- Failed to deserialize data '{payload}' into Pydantic model '
AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30).
Data as JSON: /api/errors/a38e8a89ad5b0cef.
Report an issue: GitHub.