sqlalchemy/sqlalchemy · error · NotImplementedError

Immutable objects do not support copying

Error message

Immutable objects do not support copying

What it means

Raised by Immutable.unique_params() on ClauseElement subclasses marked Immutable (notably Column, ColumnClause, Table, TableClause). These objects are structurally immutable in DQL/DML generation, so creating a copy with new bound-parameter values is disallowed by design. Calling .unique_params() therefore raises NotImplementedError instead of producing a new bound-parameter copy.

Source

Thrown at lib/sqlalchemy/sql/base.py:208

class Immutable:
    """mark a ClauseElement as 'immutable' when expressions are cloned.

    "immutable" objects refers to the "mutability" of an object in the
    context of SQL DQL and DML generation.   Such as, in DQL, one can
    compose a SELECT or subquery of varied forms, but one cannot modify
    the structure of a specific table or column within DQL.
    :class:`.Immutable` is mostly intended to follow this concept, and as
    such the primary "immutable" objects are :class:`.ColumnClause`,
    :class:`.Column`, :class:`.TableClause`, :class:`.Table`.

    """

    __slots__ = ()

    _is_immutable: bool = True

    def unique_params(self, *optionaldict: Any, **kwargs: Any) -> NoReturn:
        raise NotImplementedError("Immutable objects do not support copying")

    def params(self, *optionaldict: Any, **kwargs: Any) -> NoReturn:
        raise NotImplementedError("Immutable objects do not support copying")

    def _clone(self: _Self, **kw: Any) -> _Self:
        return self

    def _copy_internals(
        self, *, omit_attrs: Iterable[str] = (), **kw: Any
    ) -> None:
        pass


class SingletonConstant(Immutable):
    """Represent SQL constants like NULL, TRUE, FALSE"""

    _is_singleton_constant: bool = True

View on GitHub (pinned to d9b44cb731)

Solutions

  1. Do not call unique_params() on Table/Column/immutable elements; instead apply .params()/.unique_params() on the Select or other mutable parent statement that references them.
  2. If you need per-execution bind values, pass them to connection.execute(stmt, params=...) rather than mutating the immutable element.
  3. Guard generic traversal code with a check for the `_is_immutable` attribute (or `isinstance(el, Immutable)`) before calling unique_params().

Example fix

// before
stmt = my_table.unique_params({'id': 5})

// after
stmt = select(my_table).where(my_table.c.id == bindparam('id'))
conn.execute(stmt, {'id': 5})
Defensive patterns

Strategy: type-guard

Validate before calling

def can_call_unique_params(obj) -> bool:
    """Call before obj.unique_params(...) to avoid NotImplementedError on
    immutable objects (Column, ColumnClause, Table, TableClause)."""
    return not getattr(obj, "_is_immutable", False)

Type guard

from typing import Protocol

cclass SupportsParams(Protocol):
    def params(self, *a, **k): ...
    def unique_params(self, *a, **k): ...

def is_mutable_clause(obj) -> bool:
    """Narrower: True when obj supports params()/unique_params() (not Immutable)."""
    return not bool(getattr(obj, "_is_immutable", False))

Try / catch

try:
    bound = clause.unique_params(value=1)
except NotImplementedError as e:
    if "Immutable objects do not support copying" in str(e):
        # bind params on a mutable clone instead, or use a fresh literal bind
        bound = clause._clone()  # immutable _clone returns self; rebuild instead
        raise TypeError(
            f"Cannot bind params on immutable {type(clause).__name__}; "
            f"wrap in a subquery or use literal_binds"
        ) from e
    raise

Prevention

When it happens

Trigger: Calling `.unique_params(...)` directly on a Table or Column instance, or on a column-like element that is detected as immutable, e.g. `my_table.unique_params({'x': 1})` or `my_column.unique_params(x=1)`. Also triggered indirectly when generic code blindly calls unique_params() on any ClauseElement without checking mutability.

Common situations: Generic helper code that iterates over SQL elements and calls unique_params() for bind-parameter substitution; refactoring a query builder that previously used mutable TextClause/Select elements onto raw Table/Column references; migrating from older SQLAlchemy where mutability semantics differed.

Related errors


AI-assisted analysis of sqlalchemy/sqlalchemy@d9b44cb731 (2026-08-01). Data as JSON: /data/errors/9fd781ecafb387ca.json. Report an issue: GitHub.