sqlalchemy/alembic · error · LoopDetected
Self-loop is detected in revisions
Error message
Self-loop is detected in revisions (%s)
What it means
Raised as LoopDetected when a Revision's down_revision contains its own revision id, creating a self-referential loop (revision.py:1591-1592). A revision cannot be its own parent — this would make the migration history untraversable. The check runs during Revision.__init__ before the revision is registered.
Solutions
- Open the offending migration script and verify revision and down_revision are different values.
- Generate a fresh revision id: alembic revision -m 'description' produces a unique id automatically.
- If the revision was copied, ensure revision gets a new unique id and down_revision points to the actual parent.
- Run 'alembic history' after fixing to confirm the lineage is linear and loop-free.
Example fix
# before revision = 'abc123' down_revision = 'abc123' # self-loop! # after revision = 'def456' down_revision = 'abc123'
Defensive patterns
Strategy: validation
Validate before calling
def has_no_self_loop(revision, down_revision):
from alembic.util import to_tuple
if down_revision is None:
return True
return revision not in to_tuple(down_revision) Try / catch
from alembic.script.revision import LoopDetected
try:
# construct or register revision
script_dir.revision_map.add_revision(my_revision)
except LoopDetected as e:
print(f"Self-loop: revision {e.revisions} points to itself")
raise Prevention
- Never copy-paste a migration file and keep the same revision id as down_revision.
- After creating a migration, verify revision != down_revision.
- Use 'alembic revision' command rather than hand-copying files.
When it happens
Trigger: Constructing a Revision where revision == down_revision (directly or within a tuple). Occurs at revision.py:1591: 'if down_revision and revision in util.to_tuple(down_revision)'. Triggered by hand-editing a migration script's revision/down_revision to the same value, or a buggy revision id generator.
Common situations: Copy-pasting a migration file and forgetting to change the revision id while setting down_revision to the copied id. Automated tools that generate both fields from the same source. Typo where revision and down_revision are swapped to the same hash.
Related errors
- Dependency self-loop is detected in revisions
- Character(s) ' ' not allowed in revision identifier
- Can't change down_revision on a refresh operation.
- Cycle is detected in revisions
- Dependency cycle is detected in revisions
AI-assisted analysis of sqlalchemy/alembic@5551b5d35f (2026-08-11).
Data as JSON: /api/errors/13ef770942a4ff2e.
Report an issue: GitHub.
Appendix: 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 5551b5d35f)