sqlalchemy/alembic · error · TypeError

List of dictionaries expected

Error message

List of dictionaries expected

What it means

DefaultImpl.bulk_insert raises TypeError when rows is a list but its first element is not a dict. Each row must be a dictionary of column-name to value so Alembic can build the INSERT value maps; tuples, lists, or ORM objects are not accepted.

Solutions

  1. Provide each row as a dict: op.bulk_insert(table, [{'id': 1, 'name': 'a'}, ...]).
  2. Convert ORM instances: [model_to_dict(m) for m in instances] or use m.__dict__ sans _sa_instance_state.
  3. Convert tuples using the column order: [dict(zip(columns, t)) for t in tuples].

Example fix

# before
op.bulk_insert(table, [(1, 'a'), (2, 'b')])

# after
cols = ['id', 'name']
op.bulk_insert(table, [dict(zip(cols, t)) for t in [(1, 'a'), (2, 'b')]])
Defensive patterns

Strategy: type-guard

Validate before calling

def safe_bulk_insert(op, table, rows):
    if not isinstance(rows, list) or any(not isinstance(r, dict) for r in rows):
        raise TypeError('rows must be a list of dicts')
    op.bulk_insert(table, rows)

Type guard

def is_list_of_dicts(value) -> bool:
    return isinstance(value, list) and all(isinstance(r, dict) for r in value)

Prevention

When it happens

Trigger: Calling op.bulk_insert(table, [(1, 'a'), (2, 'b')]) (list of tuples); passing a list of ORM model instances; passing a list of named tuples.

Common situations: Migrating from executemany-style code that used tuples; passing ORM query results (list of model instances) directly into bulk_insert.

Related errors


AI-assisted analysis of sqlalchemy/alembic@5551b5d35f (2026-08-11). Data as JSON: /api/errors/24882bcfc2e57033. Report an issue: GitHub.

Appendix: 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 5551b5d35f)