pola-rs/polars · error · TypeError

'prefix' must be a string; found {qualified_type_name(prefix

Error message

'prefix' must be a string; found {qualified_type_name(prefix)!r}

What it means

Expr.cat.starts_with checks categorical values against a literal string prefix and requires the prefix to be a Python str. Any non-str value (bytes, int, pl.Expr) raises TypeError naming the actual type. There is no expression overload here — the prefix must be a concrete string.

Source

Thrown at py-polars/src/polars/expr/categorical.py:204

        │ mango  ┆ false      │
        │ null   ┆ null       │
        └────────┴────────────┘

        Using `starts_with` as a filter condition:

        >>> df.filter(pl.col("fruits").cat.starts_with("app"))
        shape: (1, 1)
        ┌────────┐
        │ fruits │
        │ ---    │
        │ cat    │
        ╞════════╡
        │ apple  │
        └────────┘
        """
        if not isinstance(prefix, str):
            msg = f"'prefix' must be a string; found {qualified_type_name(prefix)!r}"
            raise TypeError(msg)
        return wrap_expr(self._pyexpr.cat_starts_with(prefix))

    def ends_with(self, suffix: str) -> Expr:
        """
        Check if string representations of values end with a substring.

        .. engine-support:: in-memory, streaming, distributed

        Parameters
        ----------
        suffix
            Suffix substring.

        See Also
        --------
        contains : Check if string reprs contains a substring that matches a pattern.
        starts_with : Check if string reprs start with a substring.

View on GitHub (pinned to df599052da)

Solutions

  1. Decode bytes to str first: prefix.decode() if isinstance(prefix, bytes) else prefix
  2. Coerce at the boundary: .cat.starts_with(str(prefix))
  3. If you need per-row dynamic prefixes, cast the column to String and use .str.starts_with(other_expr) instead

Example fix

# before
pl.col('cat_col').cat.starts_with(b'app')  # TypeError

# after
pl.col('cat_col').cat.starts_with(b'app'.decode())
# per-row dynamic: pl.col('cat_col').cast(pl.String).str.starts_with(pl.col('prefix_col'))
Defensive patterns

Strategy: type-guard

Validate before calling

if isinstance(prefix, bytes):
    prefix = prefix.decode()
assert isinstance(prefix, str), 'cat.starts_with requires a str prefix'
expr = pl.col('c').cat.starts_with(prefix)

Type guard

from typing import TypeGuard

def is_str_prefix(p) -> TypeGuard[str]:
    return isinstance(p, str)

Try / catch

try:
    e = pl.col('c').cat.starts_with(prefix)
except TypeError:
    e = pl.col('c').cast(pl.String).str.starts_with(str(prefix))

Prevention

When it happens

Trigger: pl.col('c').cat.starts_with(b'app') (bytes from parquet/protobuf pipelines), .cat.starts_with(123), or .cat.starts_with(pl.lit('app')).

Common situations: Prefixes arriving as bytes from binary formats or network layers; passing expressions by habit from str-like APIs; numeric prefixes from config.

Related errors


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