Skip to main content

find python module imports

Project description

import_deps

PyPI version Python versions CI Github actions

Find python module's import dependencies.

import_deps is based on ast module from standard library, so the modules being analysed are not executed.

Install

pip install import_deps

Usage

import_deps is designed to track only imports within a known set of package and modules.

Given a package with the modules:

  • foo/__init__.py
  • foo/foo_a.py
  • foo/foo_b.py
  • foo/foo_c.py

Where foo_a.py has the following imports:

from . import foo_b
from .foo_c import obj_c

Usage (CLI)

Analyze a single file

> import_deps foo/foo_a.py
foo.foo_b
foo.foo_c

Analyze a package directory

> import_deps foo/
foo.__init__:
foo.foo_a:
  foo.foo_b
  foo.foo_c
foo.foo_b:
foo.foo_c:
  foo.__init__

JSON output

Use the --json flag to get results in JSON format:

> import_deps foo/foo_a.py --json
[
  {
    "module": "foo.foo_a",
    "imports": [
      "foo.foo_b",
      "foo.foo_c"
    ]
  }
]

For package analysis with JSON:

> import_deps foo/ --json
[
  {
    "module": "foo.__init__",
    "imports": []
  },
  {
    "module": "foo.foo_a",
    "imports": [
      "foo.foo_b",
      "foo.foo_c"
    ]
  },
  ...
]

Transitive imports

Use --all-imports with --json to include all transitive dependencies (not just direct imports):

> import_deps foo/ --json --all-imports
[
  {
    "module": "foo.foo_a",
    "imports": [
      "foo.foo_b",
      "foo.foo_c"
    ],
    "all_imports": [
      "foo.__init__",
      "foo.foo_b",
      "foo.foo_c"
    ]
  },
  ...
]

The all_imports field contains all modules that a module depends on, directly or indirectly. This is useful for understanding the full dependency tree of a module.

DOT output for visualization

Use the --dot flag to generate a dependency graph in DOT format for graphviz:

> import_deps foo/ --dot
digraph imports {
    "foo.foo_a" -> "foo.foo_b";
    "foo.foo_a" -> "foo.foo_c";
    "foo.foo_c" -> "foo.__init__";
    "foo.foo_d" -> "foo.foo_c";
    "foo.sub.sub_a" -> "foo.foo_d";
}

You can visualize the graph using graphviz:

> import_deps foo/ --dot | dot -Tpng > dependencies.png
> import_deps foo/ --dot | dot -Tsvg > dependencies.svg

The DOT output features:

  • Modules displayed as light blue rounded boxes
  • Packages grouped with dashed gray borders (clearly distinct from arrows)
  • Sub-packages nested hierarchically
  • Circular dependencies highlighted in bold red arrows

Checks

Use --check to run all checks, or --check=TYPE for a specific check:

Flag Check
--check Run all checks
--check=circular Circular dependencies only
--check=reimports Re-imports only
--check=inner Inner imports only
> import_deps foo/ --check
All checks passed.

# Or run a specific check:
> import_deps foo/ --check=circular
No circular dependencies found.

Note: When using --check without a type before PATH, use --check=all:

> import_deps --check=all foo/   # OK
> import_deps foo/ --check       # OK

This is useful for CI/CD pipelines to enforce code quality rules.

Check for circular dependencies

Use --check=circular to detect circular dependencies:

> import_deps foo/ --check=circular
Circular dependencies detected:
  foo.module_a -> foo.module_b
  foo.module_b -> foo.module_a
# (exits with code 1)

Check for re-imports

Use --check=reimports to detect re-imports. A re-import occurs when you import a name from a module that itself imported it from somewhere else, rather than importing from the original source.

# pkg/module_a.py - defines foo_func
def foo_func():
    pass

# pkg/module_b.py - re-exports foo_func
from .module_a import foo_func  # re-export

# pkg/module_c.py - BAD: imports from module_b instead of module_a
from .module_b import foo_func  # re-import!
> import_deps pkg/ --check=reimports
Re-imports detected:
  pkg.module_c: 'foo_func' imported from pkg.module_b
    -> should import from pkg.module_a
# (exits with code 1)

# If no re-imports:
> import_deps pkg/ --check=reimports
No re-imports found.

Note: __init__.py files are whitelisted since they commonly re-export symbols to provide a cleaner public API.

This is useful for:

  • Enforcing clean import hygiene in your codebase
  • Making dependencies explicit and traceable
  • Avoiding confusion about where symbols are actually defined

Check for inner imports

Use --check=inner to detect imports inside functions or classes (not at module level).

# pkg/module.py
import os  # OK - at module level

def some_function():
    import json  # BAD - inner import
    from pathlib import Path  # BAD - inner import
> import_deps pkg/ --check=inner
Inner imports detected:
  pkg/module.py:5:4: json
  pkg/module.py:6:4: pathlib
# (exits with code 1)

# If no inner imports:
> import_deps pkg/ --check=inner
No inner imports found.

This is useful for:

  • Enforcing consistent import style (all imports at module level)
  • Improving code readability and maintainability
  • Making dependencies visible at the top of each file

Topological sort

Use the --sort flag to output modules in topological order (dependencies before dependents):

> import_deps foo/ --sort
foo.__init__
foo.foo_c
bar
foo.foo_b
foo.foo_d
foo.sub.__init__
foo.foo_a
foo.sub.sub_a

Add -v/--verbose to show level and depth rankings:

> import_deps foo/ --sort -v
foo.__init__	4	1
foo.foo_c	3	2
bar	2	2
foo.foo_b	2	3
foo.foo_d	2	3
foo.sub.__init__	1	1
foo.foo_a	1	4
foo.sub.sub_a	1	4

The verbose output is tab-separated: module\tlevel\tdepth

Terminology:

  • Sources: modules not imported by anyone (entry points)
  • Sinks: modules that import nothing (leaf dependencies)
  • Level: distance from sources (reverse topological level)
  • Depth: distance from sinks (longest dependency chain)

Ordering:

  • Sorted by level DESC (deep dependencies first), then depth ASC (simpler modules first), then name
  • Dependencies always appear before modules that import them
  • Circular dependencies are handled gracefully (level=-1, depth=-1)

Handling circular dependencies

When circular dependencies exist, the sort handles them gracefully:

# If you have: A -> C -> B -> A (circular); D -> B; E (isolated)
> import_deps circular_package/ --sort -v
E	1	1
A	-1	-1
B	-1	-1
C	-1	-1
D	1	2

The ordering is:

  1. A, B, C first (nodes in the cycle, level=-1, depth=-1)
  2. D next (source with level=1, imports B which is in cycle)
  3. E last (isolated node with level=1, depth=1)

Usage (lib)

import pathlib
from import_deps import ModuleSet

# First initialise a ModuleSet instance with a list str of modules to track
pkg_paths = pathlib.Path('foo').glob('**/*.py')
module_set = ModuleSet([str(p) for p in pkg_paths])

# then you can get the set of imports
for imported in module_set.mod_imports('foo.foo_a'):
    print(imported)

# foo.foo_c
# foo.foo_b

ModuleSet

You can get a list of all modules in a ModuleSet by path or module's full qualified name.

by_path

Note that key for by_path must be exactly the as provided on ModuleSet initialization.

for mod in sorted(module_set.by_path.keys()):
    print(mod)

# results in:
# foo/__init__.py
# foo/foo_a.py
# foo/foo_b.py
# foo/foo_c.py

by_name

for mod in sorted(module_set.by_name.keys()):
    print(mod)

# results in:
# foo.__init__
# foo.foo_a
# foo.foo_b
# foo.foo_c

ast_imports(file_path)

ast_imports is a low level function that returns a list of entries for import statement in the module. The parameter file_path can be a string or pathlib.Path instance.

The return value is a list of 4-tuple items with values:

  • module name (of the "from" statement, None if a plain import)
  • object name
  • as name
  • level of relative import (number of parent, None if plain import)
from import_deps import ast_imports

ast_imports('foo.py')
# import datetime
(None, 'datetime', None, None)

# from datetime import time
('datetime', 'time', None, 0)

# from datetime import datetime as dt
('datetime', 'datetime', 'dt', 0)

# from .. import bar
(None, 'bar', None, 2)

# from .acme import baz
('acme', 'baz', None, 1)


# note that a single statement will contain one entry per imported "name"
# from datetime import time, timedelta
('datetime', 'time', None, 0)
('datetime', 'timedelta', None, 0)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

import_deps-0.5.1.tar.gz (12.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

import_deps-0.5.1-py3-none-any.whl (14.5 kB view details)

Uploaded Python 3

File details

Details for the file import_deps-0.5.1.tar.gz.

File metadata

  • Download URL: import_deps-0.5.1.tar.gz
  • Upload date:
  • Size: 12.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for import_deps-0.5.1.tar.gz
Algorithm Hash digest
SHA256 57ef3401078e7cdd1152c6be15bcc5cdc9582ab915d9cb6409da2311c07c317f
MD5 27d6a14e6b62b22846a8c93a296ee57e
BLAKE2b-256 c4b35d222a4595ba308d56c3b60f26c02227ebd28784cb5fb4f8ef9eb4dc46a8

See more details on using hashes here.

File details

Details for the file import_deps-0.5.1-py3-none-any.whl.

File metadata

  • Download URL: import_deps-0.5.1-py3-none-any.whl
  • Upload date:
  • Size: 14.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for import_deps-0.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 06af9c06fa514368666fd3f81c881f09382ac3b363d183a8b2555c6a5dea6bdc
MD5 c0dc15999364d4ab5a5f02b7fa5a368c
BLAKE2b-256 25b483fc93d68ca4ad473ff38294666359cb0c8381d2b2ef9012bde6b2c577ab

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page