Circular Dependency Detection in Agent-Generated Python and TypeScript
Agents generate circular dependencies invisibly until systems catch them at CI gates.

Circular dependencies in agent-generated code are a predictable structural outcome of how coding agents reason, and catching them requires specific tools in both Python and TypeScript along with CI gates that block a merge before a human ever opens the diff.
Why agent-generated code reliably produces circular dependencies
An AI coding agent writes one file, one function, one prompt at a time, and its sense of the surrounding codebase is bounded by whatever fits in its context window. That's the root of the problem: the agent can produce a completion that's correct on its own terms while having no view of what else in the repository already depends on the file it just touched. Architectural coherence requires seeing the whole graph, and a context-window-bound reasoner structurally cannot do that across a codebase of any real size.
The Python failure mode that results is specific and mechanical. When a.py imports from b.py, and b.py in turn imports from a.py, a hasn't finished initializing by the time b.py tries to use it, so the attribute that b.py expects to find doesn't exist yet. Python raises AttributeError: module 'a' has no attribute 'A', which read on its own resembles a typo or a missing export rather than a structural import cycle.
TypeScript hides the problem differently. The compiler never flags a circular import as an error. theme.ts imports [ButtonConfig](https://danywalls.com/how-to-detect-and-fix-circular-dependencies-in-typescript) from button.ts, and button.ts imports Theme back from theme.ts, a documented case that makes the mechanism concrete.
Multi-agent workflows make the pattern worse, not better. When separate agents work on separate files, each one's output may be locally correct while disagreeing with the other on the shape of a shared API contract, and each may introduce a reference to the other's module without either one ever "seeing" the cycle it's building. Neither agent has the full graph in view, so neither can detect what only becomes visible once both changes land together. And the effect compounds gradually rather than all at once: each individual prompt produces a change that looks clean in isolation, but the dependency graph as a whole drifts further and further from a clean, acyclic structure with every addition. No single commit looks like the cause. The cycle is the sum of many additions that each passed inspection on their own.
Why these failures are invisible until they crash production
The signals developers normally rely on to catch bad code all fail silently here. AI-generated code tends to be syntactically clean: naming conventions check out, types check out, tests pass, and none of that gives a reviewer any visual cue that the relationship between two files is broken at the architectural level. A junior engineer's bad code is visible on sight. AI-generated code carries no equivalent tell.
TypeScript's own strict mode compounds this. Turning on strict settings in tsconfig.json does nothing to warn about circular dependencies. Detecting them requires either a runtime crash or a dedicated third-party tool, because the type checker simply isn't built to see import cycles as a category of error.
The sharpest evidence of how deep this invisibility runs comes from a 2026 audit of an AI-generated Python architecture scaffold. The scaffold included a check explicitly named "Prevent circular dependencies," built using import-linter's independence contract type. That contract works by checking that none of a list of named modules imports any other module on the list. The configuration the agent generated listed only the root package, a single entry. It runs. It reports zero violations. It will always report zero violations, no matter how many cycles exist in the code it's supposedly guarding. The failure wasn't a general property of AI-generated tooling. It was specific to how the Python contract had been written, and nothing in the test output would have told a developer the difference.
A passing check is not evidence of a working check, and only reading what the contract actually compares tells a developer whether it is.
Refactoring used to catch a fair number of these problems as a side effect of ordinary housekeeping, developers cleaning up imports as they touched nearby code. As more of a codebase's volume comes from agents and less from manual passes through the same files, that incidental cleanup happens less often, and cycles that would once have been caught in routine maintenance now persist until something downstream breaks.
Detecting circular dependencies in TypeScript
No single TypeScript tool covers the whole problem, because each one looks at a different slice of the dependency graph.
Madge builds a visual graph of module dependencies and reports circular ones directly. Running npx madge --circular --extensions ts./src prints the exact cycle path, and Madge can also render an SVG of the graph for visual inspection. Its TypeScript analysis has been noted as less reliable in some cases than dpdm's.
dpdm is built for that case: a static dependency analyzer with full support for CommonJS, ESM, TypeScript path mapping, and project references. Running [dpdm](https://github.com/acrazing/dpdm) --exit-code circular:1./src/index.ts exits with a non-zero code the moment a cycle is found, which makes it straightforward to wire into a CI pipeline, and a dpdm.config.ts file keeps that configuration consistent across environments. Its -T flag ignores type-only dependencies, so a cycle that exists purely between TypeScript interfaces doesn't register as a false positive.
dependency-cruiser works at a different level: it validates and visualizes dependencies against rules a team defines itself, across JavaScript, TypeScript, and CoffeeScript. It runs as its own CI step, separate from whatever ESLint is already doing, and its no-circular rule, configured with to: { circular: true }, can forbid circular dependencies alongside orphan modules and unwanted cross-module imports in one pass.
One escape hatch cuts across all of these tools: a cycle that exists only because one file needs a TypeScript interface from another can be broken with a type-only import. Type-only imports are erased entirely at compile time and never produce a runtime module dependency, and detection tools run with the -T flag correctly decline to flag them as cycles. None of these tools, though, catches anything unless it's actually wired into the build. Making that check unavoidable rather than optional is the next step.
Detecting circular dependencies in Python: tools and a critical configuration trap
Python has capable detection tools, but the finding above shows that a correctly-installed tool can still enforce nothing if its configuration is wrong, and there's no outward sign of the gap.
The runtime symptom is the same one described earlier: a.py and b.py importing each other produces a value that's None at the moment it's needed, and the resulting AttributeError: module 'a' has no attribute 'A' reads like a bug unrelated to imports.
The heavier tool for architectural enforcement is import-linter, which Python teams use to define contracts about how modules are allowed to depend on one another. Its independence contract type is built specifically to check that a list of named modules never import each other.
That contract is exactly where the 2026 audit found the problem. The AI-generated configuration used type = independence but listed only the root package, a single module. An independence contract needs at least two modules to compare, because it checks pairs. With one entry on the list, there's no pair to test, so the contract is satisfied by definition rather than because the codebase is actually free of cycles. The check runs, passes, and reports zero violations, and nothing in that output distinguishes "this codebase has no cycles" from "this contract was never capable of finding one". A correct version of the same contract needs multiple distinct modules listed, enough for the tool to actually generate pairs to test, and any import-linter configuration an agent produces deserves a direct read before anyone trusts its result in CI.
A newer tool closes part of this gap by making the rule itself part of the test suite rather than a separate configuration file to audit. ArchUnitPython enforces architecture rules, including dependency direction and circular dependency detection, and integrates directly with pytest or any other Python test framework, with zero runtime dependencies. It was built on the reasoning that enforcing architectural boundaries in Python affects the dominant language of the AI and ML ecosystem. It started as the Python port of an existing TypeScript tool, ArchUnitTS.
ArchUnitPython's real advantage appears in agent feedback loops. An instruction like "please respect clean architecture" sitting in a project's AGENTS.md file is a suggestion an agent can quietly ignore or misread. A failing pytest assertion is neither: it names the exact dependency that has to go, in a form the agent can act on directly, something like "controllers.UserController cannot import db.UserRepository because the presentation layer cannot access the data layer directly". That's a concrete, machine-readable correction rather than a prose guideline: an agent might follow a prose rule, but it's forced to satisfy this one before the test suite goes green.
pydeps rounds out the toolkit as a visualization layer, typically run alongside import-linter to make a Python project's module dependencies visible rather than just pass or fail. None of these tools matters if the configuration behind them is wrong, and the only way to know is to read the contract's logic directly rather than trust that a green check means what it appears to mean.
Refactoring patterns that break cycles once they are found
Finding a cycle is only useful if it leads to a fix, and most circular dependencies in both Python and TypeScript resolve to one of a small handful of structural patterns.
The cleanest fix is extracting whatever the modules share into an independent, lower-level module that both import. This removes the mutual reference entirely rather than just rearranging it. The graph becomes acyclic because the shared concept now has a single owner instead of living split across files that each needed a piece of it. This is the right move whenever the real problem is that two modules share a concept that neither one should be the sole owner of.
When the cycle exists purely because one file needs a type rather than a value, TypeScript offers a narrower and simpler fix: import type. A type-only import is erased completely at compile time and never appears in the emitted JavaScript, so it can never create the runtime module dependency that causes the crash. It only works for type-level references, not for cases where one module actually needs a function or value from the other at runtime.
Where a runtime value really is needed, dependency injection breaks the cycle by having a module receive what it needs as a parameter instead of importing it directly.
The broadest version of the same idea is the Dependency Inversion Principle: high-level modules shouldn't depend on low-level modules directly, and both should depend on a shared abstraction instead. In practice that means introducing an interface or an abstract base class that both sides reference, which moves the concrete dependency out of the import graph entirely rather than just relocating it.
Put together, these patterns form a simple decision path: if the cycle is a type-only reference, use import type; if modules share a concept, extract it into a third module; if one module needs another's runtime behavior, introduce an abstraction both sides can depend on instead of each other.
Enforcing circular dependency rules in CI so agents cannot merge cycles
Detection and refactoring only matter if they happen before a cycle reaches the main branch, and in an agent workflow, that means the check has to run automatically rather than depend on a human catching it in review. An agent can touch dozens of files in a single prompt and produce a diff large enough that no reviewer can mentally reconstruct the dependency graph by reading it line by line. The check has to be structural and automated, because the human eye was never going to catch this category of problem at this scale.
One practitioner's 2026 CI guide treats this as a named, mandatory stage in the pipeline, calling it "Gate 5": madge --circular src/ or dependency-cruiser for JavaScript and TypeScript projects, paired with pydeps or import-linter for Python ones. It's a gate that either passes or blocks the merge.
For TypeScript, dpdm's exit-code behavior makes this easy to wire directly into a pipeline: dpdm --exit-code circular:1./src/index.ts exits with code 1 the moment it finds any cycle, and that single command can be dropped into a CI step with no further setup, with dpdm.config.ts keeping the rule identical across every environment it runs in. dependency-cruiser's no-circular rule, configured with to: { circular: true }, serves the same purpose as its own distinct CI step, separate from whatever ESLint pass the project already runs, and it catches whole-graph cycles that a per-file lint rule structurally cannot see.
The underlying argument is the same across both languages and every tool listed here: a detection tool that only runs when a developer remembers to run it manually will eventually get skipped, especially under the volume and speed of agent-generated pull requests. A gate that exits non-zero and fails the build is the only version of this check that actually holds once the rate of code generation outpaces the rate of human review.
Sources
- GitHub - acrazing/dpdm: Detect circular dependencies in your TypeScript projects. · GitHub
- How to Detect and Fix Circular Dependencies in Typescript
- Locate circular dependencies in TypeScript modules
- TypeScript Circular Dependency Check: A Comprehensive Guide — xjavascript.com
- Detecting Circular Dependencies in TypeScript — xjavascript.com
- Circular Dependencies Are Killing Your Codebase — Here’s How AI Can Fix Them
- Using AI Agents to Enforce Architectural Standards
- AI Coding Agents in 2026: Coherence Through Orchestration, Not Autonomy