getredash/redash · error · Exception

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

Error message

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

What it means

Raised by PythonQueryRunner.validate_result (redash/query_runner/python.py:327) when `result['rows']` is present but is not a Python list. The rows contract is strictly a list (usually of dicts keyed by column name), so tuples, generators, DataFrames, dicts, or JSON strings all fail this isinstance check.

Source

Thrown at redash/query_runner/python.py:327

    def test_connection(self):
        pass

    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

View on GitHub (pinned to ca79fe988d)

Solutions

  1. Convert to a list of dicts: `result['rows'] = df.to_dict(orient='records')` or `[dict(r) for r in rows]`
  2. If rows are JSON strings, parse first: `json.loads(payload)`
  3. Ensure each element is a dict whose keys match the `columns` names

Example fix

# before
result = {'columns': cols, 'rows': df}
# after
result = {'columns': cols, 'rows': df.to_dict(orient='records')}
Defensive patterns

Strategy: type-guard

Validate before calling

rows = result.get('rows')
if not isinstance(rows, list):
    result['rows'] = list(rows) if hasattr(rows, '__iter__') and not isinstance(rows, dict) else []

Type guard

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

Try / catch

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

Prevention

When it happens

Trigger: Assigning `result['rows'] = df` (a pandas DataFrame), `result['rows'] = tuple(...)`, a generator expression, a numpy array, or a JSON-encoded string instead of an actual list.

Common situations: Pandas users returning the DataFrame directly; users returning `df.to_json()` or `df.values`; converting via `df.itertuples()` which yields a generator.

Related errors


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