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:1305: imports werkzeug.sansio.http
dependencies to remove increased from 1 to 2

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 this version added to the problem, 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.0

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.0
File Size Uploaded
uncycle-0.1.0.tar.gz 15.2 kB Details

Built distribution (wheel)

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

Total release size: 27.0 kB

Release files / uncycle-0.1.0.tar.gz

Download URL uncycle-0.1.0.tar.gz
Size 15.2 kB
Tags Source
SHA-256 checksum
How to use checksums
abf3779fa8f55ccb4341c445be03433519b3f957c4b2ac7c96339f89624317a5
BLAKE2b-256 checksum
How to use checksums
13b9c882fa85c21b6c0e0ca92fdf0ca8b7465a40e4b7bd1a6ae872daa385ce0e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.2

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

Download URL uncycle-0.1.0-py3-none-any.whl
Size 11.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e6351e1e1f563afd70b4e953bc337413159b3970b42b5e4ca3b815dbb473da03
BLAKE2b-256 checksum
How to use checksums
51b46e4eb6131024f74964b1c7eba680fa8522b4f62f849cfedc18c893f52827
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.2

Release history Release notifications | RSS feed

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

This release

0.1.0 This release

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