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
Symmetric to the UpgradeOps case: when the rewriter walks downgrade_ops_list, each DowngradeOps must yield exactly one replacement DowngradeOps. Returning a list of length != 1 breaks the migration script's single-downgrade-tree contract.
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 44fb345033)
Solutions
- Mutate the existing DowngradeOps.ops list in place rather than replacing the DowngradeOps with a list.
- Return exactly one DowngradeOps; nest extra ops inside it.
- If multiple downgrade paths are needed, model them as separate MigrationScripts.
Example fix
// before
def process_revision(ctx, revision, directives):
directives[0].downgrade_ops = [down_ops_a, down_ops_b]
// after
def process_revision(ctx, revision, directives):
directives[0].downgrade_ops.ops.extend([op_a, op_b]) Defensive patterns
Strategy: validation
Validate before calling
downgrade_ops = directives[0].downgrade_ops assert isinstance(downgrade_ops, DowngradeOps) downgrade_ops.ops.append(new_op) # mutate, don't return a list
Type guard
def is_single_downgrade_ops(obj) -> bool:
from alembic.operations.ops import DowngradeOps
return isinstance(obj, DowngradeOps) Prevention
- Mirror the upgrade hook's discipline on the downgrade side.
- Return None from process_revision_directives and rely on in-place mutation.
- Test autogenerate produces a single downgrade block.
When it happens
Trigger: A process_revision_directives hook returns zero or more than one DowngradeOps when replacing directives[0].downgrade_ops.
Common situations: Custom hooks that try to produce multiple downgrade branches or forget to wrap a single DowngradeOps; copy-paste from an upgrade hook that mutated a different structure.
Related errors
- Can only return single object for UpgradeOps traverse
- can't return inspector as this AutogenContext has no databas
- Duplicate table keys across multiple MetaData objects: %s
- Autogenerate rendering of SQL Expression language constructs
- This MigrationScript instance has a multiple-entry list for
AI-assisted analysis of sqlalchemy/alembic@44fb345033 (2026-08-04).
Data as JSON: /data/errors/cf693b67d0b5d223.json.
Report an issue: GitHub.