getredash/redash · error · Exception

`columns` field should be of type `list`.

Error message

`columns` field should be of type `list`.

What it means

Raised by PythonQueryRunner.validate_result (redash/query_runner/python.py:329) when `result['columns']` exists but is not a list. Columns must be a Python list of column definition objects (dicts with at least `name`), so strings, tuples, pandas Index objects, or generators fail the isinstance check.

Source

Thrown at redash/query_runner/python.py:329

    def validate_result(self, result):
        """Validate the result after executing the query.

        Parameters:
        :result dict: The result dict.
        """
        if not result:
            raise Exception("local variable `result` should not be empty.")
        if not isinstance(result, dict):
            raise Exception("local variable `result` should be of type `dict`.")
        if "rows" not in result:
            raise Exception("Missing `rows` field in `result` dict.")
        if "columns" not in result:
            raise Exception("Missing `columns` field in `result` dict.")
        if not isinstance(result["rows"], list):
            raise Exception("`rows` field should be of type `list`.")
        if not isinstance(result["columns"], list):
            raise Exception("`columns` field should be of type `list`.")

    def run_query(self, query, user):
        self._current_user = user

        try:
            error = None

            code = compile_restricted(query, "<string>", "exec")

            builtins = safe_builtins.copy()
            builtins["_write_"] = self.custom_write
            builtins["__import__"] = self.custom_import
            builtins["_getattr_"] = safer_getattr
            builtins["getattr"] = safer_getattr
            builtins["_setattr_"] = guarded_setattr
            builtins["setattr"] = guarded_setattr
            builtins["_getitem_"] = self.custom_get_item
            builtins["_getiter_"] = self.custom_get_iter

View on GitHub (pinned to ca79fe988d)

Solutions

  1. Convert pandas Index to a list of dicts: `result['columns'] = [{'name': c, 'friendly_name': c, 'type': str(df[c].dtype)} for c in df.columns]`
  2. If columns arrive as JSON, parse with `json.loads` before assigning
  3. Ensure it is a plain Python list, not a tuple or string

Example fix

# before
result = {'columns': df.columns, 'rows': df.to_dict('records')}
# after
result = {'columns': [{'name': c, 'friendly_name': c} for c in df.columns], 'rows': df.to_dict('records')}
Defensive patterns

Strategy: type-guard

Validate before calling

cols = result.get('columns')
if not isinstance(cols, list):
    result['columns'] = [{'name': c} if isinstance(c, str) else c for c in cols]

Type guard

def columns_is_list(r: dict) -> bool:
    return isinstance(r.get('columns'), list)

Try / catch

try:
    validate_result(result)
except Exception as e:
    raise Exception('columns must be a list of dicts; got {}: {}'.format(type(result.get('columns')).__name__, e))

Prevention

When it happens

Trigger: Assigning `result['columns'] = df.columns` (a pandas Index), a comma-separated string like `"id,name"`, a tuple, or a dict of column types.

Common situations: Pandas users passing `df.columns` directly instead of converting it; users building a string of headers; JSON-encoded column definitions that were never parsed.

Related errors


AI-assisted analysis of getredash/redash@ca79fe988d (2026-08-28). Data as JSON: /api/errors/ec450c24d2fbb440. Report an issue: GitHub.