pandas-dev/pandas · error · TypeError

Expression objects are not copiable

Error message

Expression objects are not copiable

What it means

Raised by Expression.__copy__ (pandas/core/col.py:363). Expression objects capture a closure over a column reference (col_name) and a repr string; copying them would duplicate a deferred callable with no meaningful independent state, so copy.copy() is intentionally blocked to avoid silent aliasing bugs.

Source

Thrown at pandas/core/col.py:363

                evaluated.append((condition, replacement))
            return ser.case_when(evaluated)

        # Keep repr compact; caselist may be large.
        repr_str = f"{self!r}.case_when(...)"
        return Expression(func, repr_str)

    def __repr__(self) -> str:
        return self._repr_str or "Expr(...)"

    # Unsupported ops
    def __bool__(self) -> NoReturn:
        raise TypeError("boolean value of an expression is ambiguous")

    def __iter__(self) -> NoReturn:
        raise TypeError("Expression objects are not iterable")

    def __copy__(self) -> NoReturn:
        raise TypeError("Expression objects are not copiable")

    def __deepcopy__(self, memo: dict[int, Any] | None) -> NoReturn:
        raise TypeError("Expression objects are not copiable")


@set_module("pandas")
def col(col_name: Hashable) -> Expression:
    """
    Generate deferred object representing a column of a DataFrame.

    Any place which accepts ``lambda df: df[col_name]``, such as
    :meth:`DataFrame.assign` or :meth:`DataFrame.loc`, can also accept
    ``pd.col(col_name)``.

    .. versionadded:: 3.0.0

    Parameters
    ----------

View on GitHub (pinned to 71959b8cb9)

Solutions

  1. Do not copy the Expression; reconstruct it via `pd.col(col_name)` if you need a fresh reference.
  2. Remove the Expression from data structures that get copied, or replace it with the evaluated Series before copying.
  3. If a library force-copies, evaluate the expression against the DataFrame first so you copy a concrete Series instead.

Example fix

# before
import copy
expr = pd.col('x')
expr2 = copy.copy(expr)

# after
expr = pd.col('x')
expr2 = pd.col('x')  # construct a fresh Expression
Defensive patterns

Strategy: type-guard

Validate before calling

from pandas.core.col import Expression

def safe_copy(obj):
    if isinstance(obj, Expression):
        raise TypeError("Expression objects are not copiable; reconstruct via pd.col")
    import copy
    return copy.copy(obj)

Type guard

from pandas.core.col import Expression

def is_copyable(obj) -> bool:
    return not isinstance(obj, Expression)

Try / catch

import copy
try:
    obj2 = copy.copy(obj)
except TypeError as e:
    if 'not copiable' in str(e):
        obj2 = pd.col(obj._repr_col_name)  # reconstruct from stored name
    else:
        raise

Prevention

When it happens

Trigger: `copy.copy(pd.col('x'))`, or passing an Expression through a pipeline/library that defensively shallow-copies its inputs (e.g. some sklearn transformers, multiprocessing forks, deepcopy-heavy config loaders).

Common situations: Generic utility code that calls copy.copy on arbitrary objects. Deep-copy of a dict/list that happens to contain an Expression.

Related errors


AI-assisted analysis of pandas-dev/pandas@71959b8cb9 (2026-08-07). Data as JSON: /api/errors/67cb73c86e94a03d. Report an issue: GitHub.