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

  1. Open the offending migration script and verify revision and down_revision are different values.
  2. Generate a fresh revision id: alembic revision -m 'description' produces a unique id automatically.
  3. If the revision was copied, ensure revision gets a new unique id and down_revision points to the actual parent.
  4. 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

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


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)