deepset-ai/haystack · error · SerializationError

Serialization of nested functions is not supported.

Error message

Serialization of nested functions is not supported.

What it means

serialize_callable raises SerializationError when __qualname__ contains '<locals>', meaning the callable is a nested function defined inside another function. Such a function cannot be imported by path and therefore cannot be deserialized, so serialization is refused.

Source

Thrown at haystack/utils/callable_serialization.py:47

    Serializes a callable to its full path.

    :param callable_handle: The callable to serialize
    :return: The full path of the callable
    """
    try:
        full_arg_spec = inspect.getfullargspec(callable_handle)
        is_instance_method = bool(full_arg_spec.args and full_arg_spec.args[0] == "self")
    except TypeError:
        is_instance_method = False
    if is_instance_method:
        raise SerializationError("Serialization of instance methods is not supported.")

    # __qualname__ contains the fully qualified path we need for classmethods and staticmethods
    qualname = getattr(callable_handle, "__qualname__", "")
    if "<lambda>" in qualname:
        raise SerializationError("Serialization of lambdas is not supported.")
    if "<locals>" in qualname:
        raise SerializationError("Serialization of nested functions is not supported.")

    name = qualname or callable_handle.__name__

    # Get the full package path of the function
    module = inspect.getmodule(callable_handle)
    if module is not None:
        full_path = f"{module.__name__}.{name}"
    else:
        full_path = name

    # Serialization succeeds, but a denied builtin (e.g. `eval`) won't reload without `unsafe=True`.
    if _is_denied_builtin(callable_handle):
        logger.warning(
            "Serialized callable '{full_path}' is a builtin that is blocked during deserialization; "
            "the resulting pipeline will only be loadable with unsafe=True.",
            full_path=full_path,
        )

View on GitHub (pinned to e318778c9b)

Solutions

  1. Move the function to module level so it has an importable path
  2. Return a module-level function from the factory instead of defining it inline
  3. Promote the logic to a small callable class defined at module level

Example fix

// before
def build():
    def splitter(t): return t.split()
    return TextSplitter(splitting_function=splitter)
// after
# module level
def splitter(t): return t.split()
def build():
    return TextSplitter(splitting_function=splitter)
Defensive patterns

Strategy: validation

Validate before calling

def is_nested(fn) -> bool:
    return "<locals>" in getattr(fn, "__qualname__", "")

Type guard

import types

def is_module_level(fn) -> bool:
    return isinstance(fn, types.FunctionType) and "<locals>" not in fn.__qualname__

Try / catch

try:
    serialized = pipeline.dumps()
except SerializationError as e:
    logger.error("nested function passed as callable: %s", e)
    raise

Prevention

When it happens

Trigger: Defining a callback inside a function (e.g. def main(): def splitter(t): ...; TextSplitter(splitting_function=splitter)) then calling to_dict on the component or pipeline.

Common situations: Callbacks defined in notebooks, test fixtures, or CLI entry points; factory functions that build pipelines with locally-defined callbacks.

Related errors


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