pola-rs/polars · error · TypeError

unhashable type: 'Expr' Consider hashing '{self}.meta'.

Error message

unhashable type: 'Expr'

Consider hashing '{self}.meta'.

What it means

Expr deliberately defines __hash__ to raise TypeError: expressions are lazy AST wrappers without stable hashable identity. The message points at .meta (the MetaExpr introspection view) as the hashable/comparable handle. Dict keys, set members, and `expr in some_dict` membership all hash first, so any of those with an Expr triggers the error. Note `expr in [list]` does not hash and is unaffected.

Source

Thrown at py-polars/src/polars/expr/expr.py:319

        return self._pyexpr.to_str()

    def __repr__(self) -> str_:
        if self._pyexpr is not None:
            if len(expr_str := self._pyexpr.to_str()) > 30:
                expr_str = f"{expr_str[:30]}…"
            return f"<{self.__class__.__name__} [{expr_str!r}] at 0x{id(self):X}>"
        else:
            return "only during sphinx"

    def __str__(self) -> str_:
        if self._pyexpr is not None:
            return self._pyexpr.to_str()
        else:
            return "only during sphinx"

    def __hash__(self) -> int:
        msg = f"unhashable type: 'Expr'\n\nConsider hashing '{self}.meta'."
        raise TypeError(msg)

    def __bool__(self) -> NoReturn:
        msg = (
            "the truth value of an Expr is ambiguous"
            "\n\n"
            "You probably got here by using a Python standard library function instead "
            "of the native expressions API.\n"
            "Here are some things you might want to try:\n"
            "- instead of `pl.col('a') and pl.col('b')`, use `pl.col('a') & pl.col('b')`\n"
            "- instead of `pl.col('a') in [y, z]`, use `pl.col('a').is_in([y, z])`\n"
            "- instead of `max(pl.col('a'), pl.col('b'))`, use `pl.max_horizontal(pl.col('a'), pl.col('b'))`\n"
        )
        raise TypeError(msg)

    def __abs__(self) -> Expr:
        return self.abs()

    # operators

View on GitHub (pinned to df599052da)

Solutions

  1. Key by the string form: str(expr) (the canonical expression string)
  2. Use expr.meta (MetaExpr) as the message suggests for structural identity/equality
  3. Redesign cache keys to be plain data (column name + parameters) rather than the Expr object

Example fix

# before
cache[pl.col('a') + 1] = transform  # TypeError: unhashable

# after
cache[str(pl.col('a') + 1)] = transform  # stable string key
Defensive patterns

Strategy: type-guard

Validate before calling

import polars as pl

def expr_key(e: pl.Expr) -> str:
    return str(e)  # stable, hashable representation

cache: dict[str, object] = {}
cache[expr_key(pl.col('a') + 1)] = transform  # never hash the Expr itself

Type guard

import polars as pl
from typing import TypeGuard

def is_polars_expr(x) -> TypeGuard[pl.Expr]:
    return isinstance(x, pl.Expr)

# guard before dict/set usage
if is_polars_expr(key):
    key = str(key)

Try / catch

try:
    cache[expr] = f
except TypeError as e:
    if "unhashable type: 'Expr'" in str(e):
        cache[str(expr)] = f
    else:
        raise

Prevention

When it happens

Trigger: d[pl.col('a')] = 1; pl.col('a') in {pl.col('b')}; set([pl.col('a'), pl.col('b')]); functools.lru_cache on a function that takes an Expr; using Expr as a field of a frozen dataclass placed in a set.

Common situations: Memoising column-transform factories keyed by expression; deduplicating expressions in pipeline builders; putting Exprs into dict-based dispatch tables.

Related errors


AI-assisted analysis of pola-rs/polars@df599052da (2026-08-16). Data as JSON: /api/errors/e4e2fe636abbc201. Report an issue: GitHub.