sqlalchemy/alembic · error · LoopDetected
Self-loop is detected in revisions (%s)
Error message
Self-loop is detected in revisions (%s)
What it means
Raised as LoopDetected (revision.py:1592, kind='Self-loop') during Revision.__init__ when the revision's own id appears inside its down_revision (after tuple normalization). A migration cannot be its own parent — that would create a cycle of length one in the revision DAG, making upgrade/downgrade traversal non-terminating.
Source
Thrown at alembic/script/revision.py:1592
@classmethod
def verify_rev_id(cls, revision: str) -> None:
illegal_chars = set(revision).intersection(_revision_illegal_chars)
if illegal_chars:
raise RevisionError(
"Character(s) '%s' not allowed in revision identifier '%s'"
% (", ".join(sorted(illegal_chars)), revision)
)
def __init__(
self,
revision: str,
down_revision: str | tuple[str, ...] | None,
dependencies: str | tuple[str, ...] | None = None,
branch_labels: str | tuple[str, ...] | None = None,
) -> None:
if down_revision and revision in util.to_tuple(down_revision):
raise LoopDetected(revision)
elif dependencies is not None and revision in util.to_tuple(
dependencies
):
raise DependencyLoopDetected(revision)
self.verify_rev_id(revision)
self.revision = revision
self.down_revision = tuple_rev_as_scalar(util.to_tuple(down_revision))
self.dependencies = tuple_rev_as_scalar(util.to_tuple(dependencies))
self._orig_branch_labels = util.to_tuple(branch_labels, default=())
self.branch_labels = set(self._orig_branch_labels)
def __repr__(self) -> str:
args = [repr(self.revision), repr(self.down_revision)]
if self.dependencies:
args.append("dependencies=%r" % (self.dependencies,))
if self.branch_labels:
args.append("branch_labels=%r" % (self.branch_labels,))View on GitHub (pinned to 44fb345033)
Solutions
- Open the offending migration file and set down_revision to the actual previous revision id (or None for a base).
- If this is a new root migration, use down_revision = None.
- Validate with `alembic history` to confirm the chain is linear where expected.
Example fix
# before revision = 'abc123' down_revision = 'abc123' # after revision = 'abc123' down_revision = 'parent456'
Defensive patterns
Strategy: validation
Validate before calling
def check_no_self_loop(revision: str, down_revision):
downs = (down_revision,) if isinstance(down_revision, str) else (down_revision or ())
if revision in downs:
raise ValueError(f'revision {revision!r} cannot be its own down_revision')
check_no_self_loop(revision, down_revision) Try / catch
from alembic.script.revision import LoopDetected
try:
rev = Revision(revision, down_revision)
except LoopDetected as e:
print(f'Self-loop: {e}. Set down_revision to the real parent.') Prevention
- When templating a new migration, never copy down_revision from the revision field.
- Use `alembic revision` (autogeneration) rather than hand-writing ids.
- Review the revision/down_revision pair before saving any migration file.
When it happens
Trigger: Writing a migration file where revision = 'abc' and down_revision = 'abc'; building a Revision('xyz', down_revision='xyz') programmatically; a copy-paste error where the down_revision string duplicates the revision string.
Common situations: Duplicating a migration template and forgetting to change down_revision; automated tooling that sets down_revision to the last known id which happens to equal the new id after a naming collision.
Related errors
- Dependency self-loop is detected in revisions (%s)
- Revision %s is not an ancestor of revision %s
- Character(s) '%s' not allowed in revision identifier '%s'
- 'type' can be one of %s
- Invalid plugin expression {name!r}
AI-assisted analysis of sqlalchemy/alembic@44fb345033 (2026-08-04).
Data as JSON: /data/errors/13ef770942a4ff2e.json.
Report an issue: GitHub.