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.

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.

Appendix: 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)