pola-rs/polars · error

python function should return List[str]

Error message

python function should return List[str]

What it means

After the with_schema_modify callback runs, its return value is extracted as Vec<String>. If the callable returns anything else (list of bytes, a single string, a dict, None), extraction fails and this expect panics with 'python function should return List[str]'. A separate, proper polars error is raised only later if the length mismatches the schema.

Source

Thrown at crates/polars-python/src/lazyframe/general.rs:292

            .with_glob(glob)
            .with_raise_if_empty(raise_if_empty)
            .with_include_file_paths(include_file_paths.map(|x| x.into()))
            .with_missing_columns_policy(missing_columns.map(|x| x.0));

        if let Some(new_columns) = new_columns {
            r = r.with_column_names_overwrite(new_columns.0);
        }

        if let Some(lambda) = with_schema_modify {
            let f = |schema: Schema| {
                let iter = schema.iter_names().map(|s| s.as_str());
                Python::attach(|py| {
                    let names = PyList::new(py, iter).unwrap();

                    let out = lambda.call1(py, (names,)).expect("python function failed");
                    let new_names = out
                        .extract::<Vec<String>>(py)
                        .expect("python function should return List[str]");
                    polars_ensure!(new_names.len() == schema.len(),
                        ShapeMismatch: "The length of the new names list should be equal to or less than the original column length",
                    );
                    Ok(schema
                        .iter_values()
                        .zip(new_names)
                        .map(|(dtype, name)| Field::new(name.into(), dtype.clone()))
                        .collect())
                })
            };
            r = r.with_schema_modify(f).map_err(PyPolarsErr::from)?
        }

        Ok(r.finish().map_err(PyPolarsErr::from)?.into())
    }

    #[cfg(feature = "parquet")]
    #[staticmethod]

View on GitHub (pinned to df599052da)

Solutions

  1. Return a plain list of str with exactly one entry per input name
  2. Wrap generators/comprehensions with list(...) and ensure each element is a str
  3. Annotate the callback as Callable[[list[str]], list[str]] and test it standalone
  4. Check the length matches the input; mismatch triggers a separate ShapeMismatch error

Example fix

# before
rename = lambda names: [n.encode() for n in names]  # list[bytes] -> panic

# after
rename = lambda names: [str(n).lower() for n in names]  # list[str]
Defensive patterns

Strategy: type-guard

Validate before calling

out = rename(names)
assert type(out) is list, "must return list, not generator/tuple/str"
assert all(isinstance(n, str) for n in out), "elements must be str, not bytes"

Type guard

def returns_str_list(f, names: list[str]) -> bool:
    out = f(list(names))
    return isinstance(out, list) and len(out) == len(names) and all(type(n) is str for n in out)

Prevention

When it happens

Trigger: Callbacks returning List[bytes] (e.g. [n.encode() for n in names]), a generator, a tuple, a numpy array of objects, or a plain string instead of a list of str, passed as with_schema_modify to a lazy scan.

Common situations: Encoding renames for a different API, returning map(names) results from a function that yields non-strings, pandas-flavored callbacks returning an Index.

Related errors


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