sqlalchemy/alembic · error · DependencyLoopDetected

Dependency self-loop is detected in revisions

Error message

Dependency self-loop is detected in revisions (%s)

What it means

Raised as DependencyLoopDetected when a Revision's dependencies list contains its own revision id (revision.py:1593-1596). While down_revision defines the parent chain, dependencies declare cross-branch ordering constraints — a revision cannot depend on itself, as that creates a circular dependency that cannot be topologically sorted.

Solutions

  1. Inspect the migration script's dependencies= argument and remove the revision's own id from it.
  2. Dependencies should only reference OTHER revisions that must be applied before this one.
  3. If you need to express that this revision depends on its own down_revision chain, that's already implicit — no dependency entry needed.
  4. Run 'alembic history --verbose' after fixing to confirm no cycles remain.

Example fix

# before
revision = 'abc123'
down_revision = 'parent789'
dependencies = ['abc123', 'other456']  # self-dependency!

# after
revision = 'abc123'
down_revision = 'parent789'
dependencies = ['other456']
Defensive patterns

Strategy: validation

Validate before calling

def has_no_dependency_self_loop(revision, dependencies):
    from alembic.util import to_tuple
    if dependencies is None:
        return True
    return revision not in to_tuple(dependencies)

Try / catch

from alembic.script.revision import DependencyLoopDetected

try:
    script_dir.revision_map.add_revision(my_revision)
except DependencyLoopDetected as e:
    print(f"Dependency self-loop: {e.revisions}")
    raise

Prevention

When it happens

Trigger: Constructing a Revision where revision appears in the dependencies tuple. Occurs at revision.py:1593: 'elif dependencies is not None and revision in util.to_tuple(dependencies)'. Triggered by adding a revision's own id to its dependencies= argument.

Common situations: Adding dependencies to express cross-branch ordering and accidentally including the current revision id. Copy-pasting a dependency list that already contained the revision. Merge revisions where the revision id was also listed as a dependency for clarity but creates a loop.

Related errors


AI-assisted analysis of sqlalchemy/alembic@5551b5d35f (2026-08-11). Data as JSON: /api/errors/a9c6c86f33477fbe. Report an issue: GitHub.

Appendix: source

Thrown at alembic/script/revision.py:1596

        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,))
        return "%s(%s)" % (self.__class__.__name__, ", ".join(args))

    def add_nextrev(self, revision: Revision) -> None:
        self._all_nextrev = self._all_nextrev.union([revision.revision])

View on GitHub (pinned to 5551b5d35f)