sqlalchemy/alembic · error · ValueError

Can only return single object for UpgradeOps traverse

Error message

Can only return single object for UpgradeOps traverse

What it means

The rewriter raises ValueError when a custom rewrite function registered for an UpgradeOps directive (via @writer.rewrites) returns zero or more than one directive during traversal. The MigrationScript traversal expects each UpgradeOps container to rewrite to exactly one UpgradeOps so the upgrade script stays structurally valid.

Solutions

  1. Ensure your @writer.rewrites handler for UpgradeOps always returns exactly one UpgradeOps object.
  2. To add/remove operations, mutate the UpgradeOps.ops list in place and return the same object.
  3. If you need multiple upgrade scripts, emit them via process_revision_directives at the MigrationScript level instead of the UpgradeOps level.

Example fix

# before
@writer.rewrites(UpgradeOps)
def rewrite_upgrade(context, revision, op):
    return [op, UpgradeOps(ops=[])]  # two -> error

# after
@writer.rewrites(UpgradeOps)
def rewrite_upgrade(context, revision, op):
    op.ops.insert(0, my_extra_op)
    return op  # exactly one
Defensive patterns

Strategy: validation

Validate before calling

from alembic.operations import ops

def safe_rewrite_upgrade(handler):
    def wrapper(context, revision, op):
        result = handler(context, revision, op)
        assert isinstance(result, ops.UpgradeOps), 'must return one UpgradeOps'
        return result
    return wrapper

Type guard

from alembic.operations import ops

def is_single_upgrade_ops(value) -> bool:
    return isinstance(value, ops.UpgradeOps)

Prevention

When it happens

Trigger: Registering @writer.rewrites(UpgradeOps) and returning a list of multiple UpgradeOps, or returning None, or returning an empty list from the handler.

Common situations: Custom directive rewriting that tries to split a single UpgradeOps into several scripts; a rewrite function that conditionally returns the op or None depending on a flag.

Related errors


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

Appendix: source

Thrown at alembic/autogenerate/rewriter.py:168

        revision: _GetRevArg,
        directives: list[MigrationScript],
    ) -> None:
        self.process_revision_directives(context, revision, directives)
        for process_revision_directives in self._chained:
            process_revision_directives(context, revision, directives)

    @_traverse.dispatch_for(ops.MigrationScript)
    def _traverse_script(
        self,
        context: MigrationContext,
        revision: _GetRevArg,
        directive: MigrationScript,
    ) -> None:
        upgrade_ops_list: list[UpgradeOps] = []
        for upgrade_ops in directive.upgrade_ops_list:
            ret = self._traverse_for(context, revision, upgrade_ops)
            if len(ret) != 1:
                raise ValueError(
                    "Can only return single object for UpgradeOps traverse"
                )
            upgrade_ops_list.append(ret[0])

        directive.upgrade_ops = upgrade_ops_list

        downgrade_ops_list: list[DowngradeOps] = []
        for downgrade_ops in directive.downgrade_ops_list:
            ret = self._traverse_for(context, revision, downgrade_ops)
            if len(ret) != 1:
                raise ValueError(
                    "Can only return single object for DowngradeOps traverse"
                )
            downgrade_ops_list.append(ret[0])
        directive.downgrade_ops = downgrade_ops_list

    @_traverse.dispatch_for(ops.OpContainer)
    def _traverse_op_container(

View on GitHub (pinned to 5551b5d35f)