sqlalchemy/alembic · error · NotImplementedError

Can't drop table in batch mode

Error message

Can't drop table in batch mode

What it means

Raised as NotImplementedError by the batch operations proxy when drop_table() is called inside a with op.batch_alter_table(...) block. Batch mode reconstructs a single table by copying it; dropping a table is conceptually outside its scope, so the proxy (ApplyBatchImplType/BatchOperationsImpl at batch.py:200-201) refuses it explicitly. The companion create_table() raises the analogous 'Can't create table in batch mode' error.

Solutions

  1. Move the op.drop_table(...) call outside (after) the `with op.batch_alter_table(...)` block so it runs as a normal top-level operation.
  2. If you intended to clear the table contents, use op.execute('DELETE FROM mytable') inside the batch block instead of drop_table.
  3. If you are rewriting the whole schema, do not use batch_alter_table at all; emit the drop_table at module level.

Example fix

// before
with op.batch_alter_table('users') as batch_op:
    batch_op.drop_table('users')  # wrong scope
// after
with op.batch_alter_table('users') as batch_op:
    batch_op.alter_column('email', existing_type=sa.String())
op.drop_table('users')  # top-level call
Defensive patterns

Strategy: validation

Validate before calling

# Before entering batch mode, ensure the op you intend to call is allowed.
_BATCH_ALLOWED = {
    'alter_column', 'add_column', 'drop_column',
    'add_constraint', 'drop_constraint',
    'create_index', 'drop_index',
    'create_table_comment', 'drop_table_comment',
    'create_column_comment',
}

if op_name in {'create_table', 'drop_table'}:
    raise ValueError(f'{op_name} cannot run inside batch_alter_table; move it to top level')

Type guard

# narrow an operation name to the batch-safe set
from typing import Literal
BatchSafeOp = Literal[
    'alter_column', 'add_column', 'drop_column',
    'add_constraint', 'drop_constraint',
    'create_index', 'drop_index',
]
def is_batch_safe(op_name: str) -> bool:
    return op_name in get_args(BatchSafeOp)

Prevention

When it happens

Trigger: Calling op.drop_table('mytable') (or batch_op.drop_table(...)) while inside a `with op.batch_alter_table('mytable') as batch_op:` context manager. The batch context only proxies alter_column/add_column/drop_column/add_constraint/drop_constraint/create_index/drop_index; create_table and drop_table are deliberately stubbed out.

Common situations: Developers copy a sequence of op.* calls (including a table drop) and paste them inside a batch_alter_table block by mistake, or refactor an existing migration and forget that the drop belongs at the top level. Common when SQLite workarounds force batch mode for unrelated alters.

Related errors


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

Appendix: source

Thrown at alembic/operations/batch.py:201

        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:
        self.impl = impl
        self.table = table  # this is a Table object
        self.table_args = table_args

View on GitHub (pinned to 5551b5d35f)