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._traverse_script walks a MigrationScript's upgrade_ops_list; for each UpgradeOps it calls the user's process_revision_directives hook and expects exactly one replacement UpgradeOps object back. Returning zero or multiple ops would break the in-place rewrite contract (the script must still have a single upgrade ops tree), so the length check raises.
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 44fb345033)
Solutions
- Have the hook mutate the single UpgradeOps it received (append ops to its .ops list) rather than returning a new list.
- Return exactly one UpgradeOps instance; nest additional operations inside it.
- If multiple upgrade paths are genuinely needed, restructure so they are separate MigrationScript entries, not multiple UpgradeOps in one script.
Example fix
// before
def process_revision(ctx, revision, directives):
upgrade_ops = directives[0].upgrade_ops
upgrade_ops.ops = [op1, op2] # OK
return [upgrade_ops, extra_ops] # wrong: returns 2
// after
def process_revision(ctx, revision, directives):
upgrade_ops = directives[0].upgrade_ops
upgrade_ops.ops.append(extra_op)
# return nothing; mutation is enough Defensive patterns
Strategy: validation
Validate before calling
# inside process_revision_directives upgrade_ops = directives[0].upgrade_ops assert isinstance(upgrade_ops, UpgradeOps) upgrade_ops.ops.append(new_op) # mutate in place, do not return a list
Type guard
def is_single_upgrade_ops(obj) -> bool:
from alembic.operations.ops import UpgradeOps
return isinstance(obj, UpgradeOps) Prevention
- Mutate directives[0].upgrade_ops.ops in place; do not replace the UpgradeOps with a list.
- Never return a list from process_revision_directives; either return None or a single object.
- Add a smoke test that runs autogenerate with your hook.
When it happens
Trigger: Passing a process_revision_directives callable to command.revision / RevisionContext that returns a list of more than one (or zero) UpgradeOps in place of the single directive it received. Typically happens when a custom hook appends or replaces directives[0].upgrade_ops with a list rather than a single UpgradeOps instance.
Common situations: Custom process_revision_directives that splits work into multiple upgrade ops trees; a hook that mistakenly returns [op1, op2] instead of op1 with op2 nested as an op inside it.
Related errors
- Can only return single object for DowngradeOps 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/1bc2ea98f4fb5f5e.json.
Report an issue: GitHub.