pola-rs/polars · error · CopyNotAllowedError

bitmask must be constructed

Error message

bitmask must be constructed

What it means

When a column declares USE_NAN nullability (nulls represented by NaN, typical for floats), Polars must compute a validity mask via data.is_not_nan() — a new allocation, not zero-copy. With allow_copy=False this raises CopyNotAllowedError('bitmask must be constructed').

Solutions

  1. Call pl.from_dataframe(obj, allow_copy=True) so the validity mask can be computed.
  2. Have the producer export a proper bitmask (USE_BITMAP) validity buffer instead of USE_NAN.
  3. Catch CopyNotAllowedError and retry with allow_copy=True.

Example fix

# before
df = pl.from_dataframe(obj, allow_copy=False)  # USE_NAN column -> CopyNotAllowedError

# after
df = pl.from_dataframe(obj, allow_copy=True)   # builds data.is_not_nan() mask
Defensive patterns

Strategy: fallback

Validate before calling

def uses_nan_nulls(icdf) -> bool:
    return any(
        icdf.get_column(i).describe_null()[0] == ColumnNullType.USE_NAN
        for i in range(icdf.num_columns())
    )

allow_copy = not uses_nan_nulls(obj.__dataframe__())

Try / catch

from polars.interchange.protocol import CopyNotAllowedError

try:
    df = pl.from_dataframe(obj, allow_copy=False)
except CopyNotAllowedError:
    df = pl.from_dataframe(obj, allow_copy=True)  # allows data.is_not_nan() mask

Prevention

When it happens

Trigger: pl.from_dataframe(obj, allow_copy=False) where describe_null()[0] == ColumnNullType.USE_NAN for some column — producers that conflate NaN and null, or were asked for nan_as_null semantics.

Common situations: Ingesting float columns from producers that mark missing values as NaN; zero-copy pipelines that expect bitmask validity but receive NaN-based validity.

Related errors


AI-assisted analysis of pola-rs/polars@9b5d73fd00 (2026-08-19). Data as JSON: /api/errors/470b61b5b43d1df8. Report an issue: GitHub.

Appendix: source

Thrown at py-polars/src/polars/interchange/from_dataframe.py:276

        if validity_buffer_info is None:
            return None
        buffer = validity_buffer_info[0]
        return _construct_validity_buffer_from_bitmask(
            buffer, null_value, column.size(), offset, allow_copy=allow_copy
        )

    elif null_type == ColumnNullType.USE_BYTEMASK:
        if validity_buffer_info is None:
            return None
        buffer = validity_buffer_info[0]
        return _construct_validity_buffer_from_bytemask(
            buffer, null_value, allow_copy=allow_copy
        )

    elif null_type == ColumnNullType.USE_NAN:
        if not allow_copy:
            msg = "bitmask must be constructed"
            raise CopyNotAllowedError(msg)
        return data.is_not_nan()

    elif null_type == ColumnNullType.USE_SENTINEL:
        if not allow_copy:
            msg = "bitmask must be constructed"
            raise CopyNotAllowedError(msg)

        sentinel = pl.Series([null_value])
        try:
            if column_dtype.is_temporal():
                sentinel = sentinel.cast(column_dtype)
            return data != sentinel  # noqa: TRY300
        except InvalidOperationError as e:
            msg = f"invalid sentinel value for column of type {column_dtype}: {null_value!r}"
            raise TypeError(msg) from e

    else:
        msg = f"unsupported null type: {null_type!r}"

View on GitHub (pinned to 9b5d73fd00)