sqlalchemy/alembic · error · TypeError

List expected

Error message

List expected

What it means

Operations.bulk_insert (and the underlying impl.bulk_insert) require rows to be a Python list; passing a single dict, a tuple, a generator, or None is rejected because the implementation iterates and indexes by position to build the insert.

Source

Thrown at alembic/ddl/impl.py:484

        self._exec(schema.SetTableComment(table))

    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
                            )

View on GitHub (pinned to 44fb345033)

Solutions

  1. Wrap the data in a list: op.bulk_insert(table, [{'id': 1, 'name': 'x'}]).
  2. Convert tuples/generators to a list before the call: op.bulk_insert(table, list(rows)).

Example fix

// before
op.bulk_insert(account_table, {'id': 1, 'name': 'acme'})
// after
op.bulk_insert(account_table, [{'id': 1, 'name': 'acme'}])
Defensive patterns

Strategy: type-guard

Validate before calling

rows = list(rows) if not isinstance(rows, list) else rows
assert isinstance(rows, list), "bulk_insert requires a list of rows"

Type guard

def is_row_list(rows) -> bool:
    return isinstance(rows, list)

Prevention

When it happens

Trigger: Calling op.bulk_insert(table, {'id': 1, 'name': 'x'}) (a single dict) instead of a list; passing a generator expression; passing a tuple; passing None.

Common situations: Mistakenly passing one record as a dict because that's how you'd call table.insert().values(...); passing a tuple from a function that returns one; forgetting to wrap a single row in [] before upgrade.

Related errors


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