Skip to main content

uncycle

A Python tool to find the fewest import statements to remove to break all circular imports.

A five-module import graph with two cycles; one edge, shown dashed, breaks both

It gives you short and actionable feedback to structure your Python package better.

It works by computing the so-called Feedback Arc Set on the graph of Python modules (nodes) and import statements (edges).

Usage

uncycle [--exclude REGEX] [--inline] [--baseline OLD] [--dump-graph FILE] PACKAGE

Listing problematic import statements

Use uncycle path/to/pkg to list the minimal import statements to delete to break all circular imports:

$ uncycle werkzeug-3.1.8/src/werkzeug
werkzeug-3.1.8/src/werkzeug/http.py:1442: imports werkzeug.datastructures
werkzeug-3.1.8/src/werkzeug/http.py:1443: imports werkzeug.sansio.http
2 dependencies to remove

Finding regressions

Use --baseline to see whether a new commit or version regresses the number of dependencies to remove:

$ uncycle Werkzeug-2.2.0/src/werkzeug --baseline Werkzeug-2.1.2/src/werkzeug
Werkzeug-2.2.0/src/werkzeug/http.py:1304: imports werkzeug.datastructures
Werkzeug-2.2.0/src/werkzeug/http.py:1305: imports werkzeug.sansio.http
dependencies to remove increased from 1 to 2

The import statements that still have to go are listed either way; in red the ones this version added.

This check is useful in CI:

- uses: actions/checkout@v5
  with: { ref: "${{ github.event.pull_request.base.sha }}", path: old }
- uses: actions/checkout@v5
  with: { path: new }
- run: pip install uncycle
- run: uncycle new/src/mypkg --baseline old/src/mypkg

Install

pip install uncycle

Options

  • --exclude REGEX: exclude certain modules, for example: '^app\.(vendor|tests)\b'.
  • --inline: also count imports inside functions and classes.
  • --baseline OLD: an older version of the package. Lists the import statements that still have to go, highlights the ones this version added, and exits 1 if more dependencies have to go than before.
  • --dump-graph FILE: write the import graph to FILE (- for stdout) instead of solving it.
  • --format json|text: format of the dumped graph. Defaults to text when FILE ends in .txt, otherwise json.

Exit status

  • 0: the dependencies were listed, or no more of them have to go than in the baseline.
  • 1: more dependencies have to go than in the baseline.
  • 2: a file could not be read or parsed, or the arguments were invalid.

Python API

import uncycle

graph = uncycle.build_graph("src/app", exclude=r"^app\.tests\b")
fas = uncycle.minimum_feedback_arc_set(graph)
print(graph.names(fas))  # [('app.db', 'app.models')]

The import graph

The import graph is constructed statically using AST parsing. Imports under if TYPE_CHECKING and if __name__ == "__main__" are dropped. Dynamic imports inside functions and classes only count with --inline.

Notes

Imports of submodules are never reported to make things actionable. Consider a module foo that imports a submodule foo.bar to re-export some of its API: it's practically impossible to eliminate this import. Technically this means that we're computing a constrained version of the feedback arc set.

Also notice there are typically many optimal solutions, but only one (arbitrary) solution is printed. For example a trivial cycle a -> b -> c -> a can be made acyclic by removing any edge.

See also

  • pylint's cyclic-import, pycycle and import-linter report every cycle they find, one chain of modules per cycle. In a package with many cycles that is a long list; uncycle reports the few imports that break all of them.
  • The minimum is exact, computed with clingo.

Metadata

Release files for uncycle 0.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for uncycle 0.1.3
File Size Uploaded
uncycle-0.1.3.tar.gz 18.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for uncycle 0.1.3
File Interpreter ABI Platform
uncycle-0.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 31.9 kB

Release files / uncycle-0.1.3.tar.gz

Download URL uncycle-0.1.3.tar.gz
Size 18.5 kB
Tags Source
SHA-256 checksum
How to use checksums
1e3abc38ec43905b5b1d35ca853a1331b3a4ef3db3d2b3623b0e4e0ae62b4151
BLAKE2b-256 checksum
How to use checksums
84c3df8ee0ad2841291925c77cf25239d883eba63e499c798179930d1c7be014
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Release files / uncycle-0.1.3-py3-none-any.whl

Download URL uncycle-0.1.3-py3-none-any.whl
Size 13.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8dff9f1d1d552d3104eba6a37963f2dd0fa03ecc35ab4d98effd07250a593547
BLAKE2b-256 checksum
How to use checksums
692e90bc6fc7267a023696141a996f0142adf28c62fdba759d0da5797f972172
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Release history Release notifications | RSS feed

This release

0.1.3 This release

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page