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.2

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.2
File Size Uploaded
uncycle-0.1.2.tar.gz 18.2 kB Details

Built distribution (wheel)

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

Total release size: 31.4 kB

Release files / uncycle-0.1.2.tar.gz

Download URL uncycle-0.1.2.tar.gz
Size 18.2 kB
Tags Source
SHA-256 checksum
How to use checksums
f23c69448e5bcb84020b968526e4e6cafe3c2eda578d8deffa7add8b20c02abc
BLAKE2b-256 checksum
How to use checksums
9f27b7022e00943561472106d0d35c25389766fd9de6d1003232a1643305a057
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.2-py3-none-any.whl

Download URL uncycle-0.1.2-py3-none-any.whl
Size 13.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4fedab771a248bd6d61de85b72893f0d527dcc46421305fc7215456dc26b27e1
BLAKE2b-256 checksum
How to use checksums
1261097ac985b026cc1ead3e9c652e0234cd1f730d221e17fb845ce3d9c52006
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

0.1.3

2 release files

This release

0.1.2 This release

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