sqlalchemy/alembic · error · ValueError

Can only return single object for DowngradeOps traverse

Error message

Can only return single object for DowngradeOps traverse

What it means

The rewriter raises ValueError when a custom rewrite registered for a DowngradeOps directive returns zero or more than one directive during traversal. As with UpgradeOps, the traversal contract requires each DowngradeOps container to map to exactly one DowngradeOps to preserve the migration script structure.

Solutions

  1. Ensure your @writer.rewrites handler for DowngradeOps always returns exactly one DowngradeOps.
  2. Mutate op.ops in place to add or remove downgrade steps, then return the same op.
  3. Return an empty DowngradeOps(ops=[]) if you genuinely want a no-op downgrade, never None or a multi-element list.

Example fix

# before
@writer.rewrites(DowngradeOps)
def rewrite_down(context, revision, op):
    if not op.ops:
        return None  # error
    return op

# after
@writer.rewrites(DowngradeOps)
def rewrite_down(context, revision, op):
    return op  # always exactly one, even if ops is empty
Defensive patterns

Strategy: validation

Validate before calling

from alembic.operations import ops

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

Type guard

from alembic.operations import ops

def is_single_downgrade_ops(value) -> bool:
    return isinstance(value, ops.DowngradeOps)

Prevention

When it happens

Trigger: Registering @writer.rewrites(DowngradeOps) and returning a list of multiple DowngradeOps objects, None, or an empty list.

Common situations: Custom downgrade rewriting that attempts to fork a downgrade into multiple branches; a rewrite that returns [] when it has 'nothing to do'.

Related errors


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

Appendix: source

Thrown at alembic/autogenerate/rewriter.py:179

        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(
        self,
        context: MigrationContext,
        revision: _GetRevArg,
        directive: OpContainer,
    ) -> None:
        self._traverse_list(context, revision, directive.ops)

    @_traverse.dispatch_for(ops.MigrateOperation)
    def _traverse_any_directive(
        self,
        context: MigrationContext,

View on GitHub (pinned to 5551b5d35f)