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
- Ensure your @writer.rewrites handler for UpgradeOps always returns exactly one UpgradeOps object.
- To add/remove operations, mutate the UpgradeOps.ops list in place and return the same object.
- 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
- Always return exactly one UpgradeOps from @writer.rewrites(UpgradeOps) handlers.
- Mutate op.ops in place rather than returning a new list.
- Add a unit test asserting the rewrite returns a single UpgradeOps for representative inputs.
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
- Can only return single object for DowngradeOps traverse
- Autogenerate rendering of SQL Expression language…
- can't return inspector as this AutogenContext has no…
- Duplicate table keys across multiple MetaData objects
- no dispatch function for object
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)