sqlalchemy/alembic · error · TypeError

List of dictionaries expected

Error message

List of dictionaries expected

What it means

Even when rows is a list, each element must be a dict mapping column name to value, because bulk_insert builds an executemany insert. A list whose first element is not a dict (e.g. a list of tuples, a list of model instances, or a list of strings) cannot be mapped to columns.

Source

Thrown at alembic/ddl/impl.py:486

    def drop_table_comment(self, table: Table) -> None:
        self._exec(schema.DropTableComment(table))

    def create_column_comment(self, column: Column[Any]) -> None:
        self._exec(schema.SetColumnComment(column))

    def drop_index(self, index: Index, **kw: Any) -> None:
        self._exec(schema.DropIndex(index, **kw))

    def bulk_insert(
        self,
        table: TableClause | Table,
        rows: list[dict],
        multiinsert: bool = True,
    ) -> None:
        if not isinstance(rows, list):
            raise TypeError("List expected")
        elif rows and not isinstance(rows[0], dict):
            raise TypeError("List of dictionaries expected")
        if self.as_sql:
            for row in rows:
                self._exec(
                    table.insert()
                    .inline()
                    .values(
                        **{
                            k: (
                                sqla_compat._literal_bindparam(
                                    k, v, type_=table.c[k].type
                                )
                                if not isinstance(
                                    v, sqla_compat._literal_bindparam
                                )
                                else v
                            )
                            for k, v in row.items()
                        }

View on GitHub (pinned to 44fb345033)

Solutions

  1. Convert each row to a dict keyed by column name, e.g. [{'id': i, 'name': n} for i, n in rows].
  2. If you have ORM instances, extract their dict: [{c.name: getattr(o, c.name) for c in table.columns} for o in objects].
  3. For positional data, map explicitly with the column order from table.columns.keys().

Example fix

// before
op.bulk_insert(table, [(1, 'a'), (2, 'b')])
// after
op.bulk_insert(table, [{'id': 1, 'name': 'a'}, {'id': 2, 'name': 'b'}])
Defensive patterns

Strategy: type-guard

Validate before calling

rows = list(rows)
assert all(isinstance(r, dict) for r in rows), "bulk_insert requires a list of dicts"

Type guard

def is_dict_list(rows) -> bool:
    return isinstance(rows, list) and (not rows or isinstance(rows[0], dict))

Prevention

When it happens

Trigger: Calling op.bulk_insert(table, [(1, 'a'), (2, 'b')]) (list of tuples); a list of ORM instances; a list of lists; a list whose first row happens to be None or a non-dict.

Common situations: Porting a raw cursor.executemany call that used positional tuples; passing ORM objects directly instead of their __dict__; an empty-but-typed list followed by malformed first row.

Related errors


AI-assisted analysis of sqlalchemy/alembic@44fb345033 (2026-08-04). Data as JSON: /data/errors/24882bcfc2e57033.json. Report an issue: GitHub.