sqlalchemy/alembic · critical · DependencyCycleDetected

Dependency cycle is detected in revisions

Error message

Dependency cycle is detected in revisions (%s)

What it means

Raised as DependencyCycleDetected (subclass of CycleDetected, revision.py:100-104) by _detect_cycles (revision.py:293-294) when there are no _real_heads or _real_bases in the dependency graph (built from _all_down_revisions, which includes BOTH down_revision AND the depends_on list). Unlike 36/37 (pure down_revision), this covers inter-revision `depends_on` dependencies. With no real terminus the dependency graph is cyclic.

Solutions

  1. Inspect each reported revision's `depends_on` argument and break the mutual dependency.
  2. Ensure depends_on references strictly earlier revisions (or branches) and never forms a loop.
  3. Run `alembic heads` / `alembic history` after fixing to confirm the dependency graph is acyclic.

Example fix

// before
revision = 'aaa'
down_revision = 'base'
depends_on = ['bbb']
# revision bbb: depends_on = ['aaa']  # mutual cycle
// after
revision = 'aaa'
down_revision = 'base'
depends_on = None  # or a strictly-earlier revision
Defensive patterns

Strategy: validation

Validate before calling

# Detect cycles in the depends_on dependency graph.
from alembic.script import ScriptDirectory

def detect_dependency_cycle(config):
    sd = ScriptDirectory.from_config(config)
    revs = {r.revision: r for r in sd.walk_revisions()}
    color = {}
    def dfs(r):
        if color.get(r) == 'gray':
            raise ValueError(f'Dependency cycle at {r}')
        if color.get(r) == 'black':
            return
        color[r] = 'gray'
        for dep in getattr(revs[r], 'depends_on', None) or ():
            if dep in revs:
                dfs(dep)
        color[r] = 'black'
    for r in revs:
        dfs(r)

detect_dependency_cycle(config)

Type guard

def depends_on_is_acyclic(revs: dict) -> bool:
    color = {}
    def dfs(r):
        if color.get(r) == 'gray':
            return False
        if color.get(r) == 'black':
            return True
        color[r] = 'gray'
        for dep in getattr(revs[r], 'depends_on', None) or ():
            if dep in revs and not dfs(dep):
                return False
        color[r] = 'black'
        return True
    return all(dfs(r) for r in revs)

Try / catch

from alembic.script.revision import DependencyCycleDetected

try:
    alembic.command.upgrade(config, 'head')
except DependencyCycleDetected as e:
    print('Dependency cycle in depends_on among:', e.revisions)

Prevention

When it happens

Trigger: Two revisions declare depends_on that point at each other (A depends_on B, B depends_on A); or a depends_on chain that loops back. The dependency-only traversal (ignoring the linear down_revision) finds no head/base, so the whole set is flagged as a dependency cycle.

Common situations: Multi-branch projects using `depends_on` in revision directives; merge dependencies wired incorrectly; hand-authored revisions that cross-reference each other for ordering.

Related errors


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

Appendix: source

Thrown at alembic/script/revision.py:294

            for rev in self._iterate_related_revisions(
                lambda r: r._versioned_down_revisions,
                heads,
                map_=cast(_RevisionMapType, rev_map),
            )
        }.intersection(
            rev.revision
            for rev in self._iterate_related_revisions(
                lambda r: r.nextrev,
                bases,
                map_=cast(_RevisionMapType, rev_map),
            )
        )
        deleted_revs = set(rev_map.keys()) - total_space
        if deleted_revs:
            raise CycleDetected(sorted(deleted_revs))

        if not _real_heads or not _real_bases:
            raise DependencyCycleDetected(list(rev_map))
        total_space = {
            rev.revision
            for rev in self._iterate_related_revisions(
                lambda r: r._all_down_revisions,
                _real_heads,
                map_=cast(_RevisionMapType, rev_map),
            )
        }.intersection(
            rev.revision
            for rev in self._iterate_related_revisions(
                lambda r: r._all_nextrev,
                _real_bases,
                map_=cast(_RevisionMapType, rev_map),
            )
        )
        deleted_revs = set(rev_map.keys()) - total_space
        if deleted_revs:
            raise DependencyCycleDetected(sorted(deleted_revs))

View on GitHub (pinned to 5551b5d35f)