sqlalchemy/alembic · error · NotImplementedError

Can't create table in batch mode

Error message

Can't create table in batch mode

What it means

Inside a batch_alter_table context the BatchOperationsImpl queues column/constraint/index ops to be replayed against the table being rebuilt; create_table is not one of them because the batch is already scoped to one existing table. Calling batch_op.create_table(...) is mapped to a method that raises immediately.

Source

Thrown at alembic/operations/batch.py:198

        self.batch.append(("drop_constraint", (const,), {}))

    def rename_table(self, *arg, **kw):
        self.batch.append(("rename_table", arg, kw))

    def create_index(self, idx: Index, **kw: Any) -> None:
        self.batch.append(("create_index", (idx,), kw))

    def drop_index(self, idx: Index, **kw: Any) -> None:
        self.batch.append(("drop_index", (idx,), kw))

    def create_table_comment(self, table):
        self.batch.append(("create_table_comment", (table,), {}))

    def drop_table_comment(self, table):
        self.batch.append(("drop_table_comment", (table,), {}))

    def create_table(self, table):
        raise NotImplementedError("Can't create table in batch mode")

    def drop_table(self, table):
        raise NotImplementedError("Can't drop table in batch mode")

    def create_column_comment(self, column):
        self.batch.append(("create_column_comment", (column,), {}))


class ApplyBatchImpl:
    def __init__(
        self,
        impl: DefaultImpl,
        table: Table,
        table_args: tuple,
        table_kwargs: dict[str, Any],
        reflected: bool,
        partial_reordering: tuple = (),
    ) -> None:

View on GitHub (pinned to 44fb345033)

Solutions

  1. Call op.create_table(...) at the top level of upgrade()/downgrade(), outside any batch_alter_table context.
  2. If the intent is to redefine the batched table, use add_column / create_*_constraint inside the batch, or recreate='always'.
  3. Split the migration: create the new table first, then batch_alter_table the existing one.

Example fix

// before
with op.batch_alter_table('users') as batch_op:
    batch_op.create_table('audit', sa.Column('id', sa.Integer))
// after
op.create_table('audit', sa.Column('id', sa.Integer))
with op.batch_alter_table('users') as batch_op:
    batch_op.add_column(sa.Column('email', sa.String))
Defensive patterns

Strategy: type-guard

Validate before calling

from alembic.operations.base import BatchOperations
if isinstance(batch_op, BatchOperations):
    raise RuntimeError("create_table not allowed in batch mode; call op.create_table outside the batch context")

Type guard

def is_batch_op(obj) -> bool:
    from alembic.operations.base import BatchOperations
    return isinstance(obj, BatchOperations)

Prevention

When it happens

Trigger: Calling batch_op.create_table(table_obj) (or a dynamically-routed create operation) inside a 'with op.batch_alter_table("t") as batch_op:' block, instead of calling op.create_table at the top level.

Common situations: Autogenerated output that mistakenly nested a create_table in a batch block; a developer assuming batch_op mirrors the full Operations API; refactoring a migration by moving a create_table inside an existing batch_alter_table.

Related errors


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