n8n-io/n8n · error · AttributeError

read-only

Error message

read-only

What it means

AttributeError('read-only') is raised by _ImmutableBuiltins.__setattr__ when sandboxed user code attempts to assign to an attribute on the builtins proxy, e.g. 'print = something' through attribute syntax. The _ImmutableBuiltins class wraps the filtered builtins dict to prevent user code from mutating global builtins, while still allowing read access.

Source

Thrown at packages/@n8n/task-runner-python/src/task_executor.py:581

                return filtered.keys()

            def values(self):
                return filtered.values()

            def items(self):
                return filtered.items()

            def get(self, key, default=None):
                return filtered.get(key, default)

            def __getattr__(self, name):
                try:
                    return filtered[name]
                except KeyError:
                    raise AttributeError(name) from None

            def __setattr__(self, name, value):
                raise AttributeError("read-only")

            def __delattr__(self, name):
                raise AttributeError("read-only")

            def __repr__(self):
                return f"ImmutableBuiltins({len(filtered)} keys)"

        return _ImmutableBuiltins()

    @staticmethod
    def _sanitize_sys_modules(security_config: SecurityConfig):
        safe_modules = {
            "builtins",
            "__main__",
            "sys",
            "traceback",
            "linecache",
            "importlib",

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Do not attempt to reassign builtins; instead bind a local variable (e.g. 'my_print = ...; my_print(...)') or wrap the call inside a helper function in user code.
  2. If redirection of output is the goal, use the runner's supported print_args / stdout capture mechanism rather than replacing print.
  3. Audit any imported third-party code for builtins-mutation patterns and replace them with non-mutating equivalents.

Example fix

// before
print = lambda *a: None  // mutates builtins proxy -> AttributeError
// after
_my_print = lambda *a: None
_my_print('debug')
Defensive patterns

Strategy: validation

Validate before calling

# Reject snippets that assign to builtins attribute-style.
import ast, re
forbidden = re.compile(r'^\\s*builtins\\.\\w+\\s*=')
if any(forbidden.match(line) for line in user_code.splitlines()):
    raise ValueError('do not assign to builtins')

Try / catch

try:
    exec_sandboxed(user_code)
except AttributeError as e:
    if str(e) == 'read-only':
        raise UserError('builtins are immutable in the sandbox')
    raise

Prevention

When it happens

Trigger: User Python code in the sandbox does something like 'builtins.print = my_print' (or relies on attribute assignment to the builtins object exposed by the sandbox), triggering __setattr__ on the immutable proxy.

Common situations: A user tries to monkey-patch a builtin for convenience (e.g. redirecting print), imports code that patches builtins at module load, or runs a snippet copy-pasted from outside n8n that mutates globals.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/8a84388d2a1efce5. Report an issue: GitHub.