pydeps cuts Python dependency graphs at two hops and hands them to Graphviz
Python Module Dependency graphs
At a glance
- What is it?
- pydeps traces the imports of a Python package, writes dot source and leaves the drawing to Graphviz, but the default view stops two hops from the target and the usage text in the README disagrees with itself. A command line tool for engineers who want to read import structure, not for tracing runtime behaviour.
- Who is it for?
- pydeps earns a place on a codebase where import structure has grown tangled and two hops cannot explain it, provided you install Graphviz yourself and run `--max-bacon` with 0 at least once. It is the wrong tool when you need runtime behaviour rather than traced imports, or on a machine with no dot command on the path.
- Can I use it commercially?
- Yes. BSD-2-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 22 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 6, 2026, and from our analysis. They are not legal advice.
Editorial analysis
pydeps hands its graph to an external dot binary
pydeps does not draw anything on its own. It traces imports, assembles a graph, writes dot source, and shells out to Graphviz, so a working dot command on the path is a hard requirement rather than a nicety. setup.py registers one console script for the whole job:
entry_points={
'console_scripts': [
'pydeps = pydeps.pydeps:pydeps',
]
},`pip install pydeps` pulls in a single runtime dependency, which says a good deal about how little of the drawing pipeline actually lives in Python. The render step is narrow by design, and four flags decide what happens to the image file:
-T FORMAT output format (svg|png)
--display PROGRAM program to use to display the graph (png or svg file depending on the T parameter)
--noshow, --no-show don't call external program to display graph
--no-output don't create .svg/.png file, implies --no-show (-t/-o will be ignored)Taken one at a time, the format flag accepts svg or png and nothing else, the display flag chooses which program opens the result, the no-show flag writes the file and leaves it alone, and the last one creates no image at all and displays nothing either.
Every stage in between can be inspected or redirected. `--show-dot`, `--dot-output`, `--nodot` control the conversion: the first prints the generated dot code, the second writes it to a file, the third skips the conversion and leaves you with the trace alone.
The analysis one step earlier works the same way. `--show-deps`, `--show-raw-deps`, `--deps-output` print the dependency analysis, print it a second time before skip rules are removed, and send it to a file instead of the screen.
Two hops by default, so the first graph is already pruned
The number that decides what your first picture contains sits in one flag. `--max-bacon INT` excludes nodes that are more than n hops away, its default is 2, and 0 means infinite, so an unconfigured run over a real package shows the immediate neighbourhood and hides everything behind it. Raising that limit to 0 turns the same command into a full transitive dump, which on a large package is a much heavier document to read and a far bigger file to draw.
Two more filters work on shape rather than distance:
--noise-level INT exclude sources or sinks with degree greater than noise-level
--max-bacon INT exclude nodes that are more than n hops away (default=2, 0 -> infinite)
--max-module-depth INT coalesce deep modules to at most n levels`--max-module-depth INT` coalesces deep modules to at most n levels, folding a tall package tree into fewer nodes so the drawing stays legible.
`--noise-level INT` deserves a second read, because its effect runs against its name. Sources or sinks with a degree greater than the number you pass are the ones excluded, so a higher value strips more of the leaves and a lower one keeps them. Set it once, then work out much later what the digit meant.
A folder on disk wins over an installed package
Everything hangs off one positional argument, `fname`, which takes either a path or the name of an importable module. The forms below cover a single file, a directory relative to the current directory, that same directory named as a package from inside its parent, an installed package from anywhere, and a module inside it:
shell> pydeps src/myapp/main.py # a single file
shell> pydeps src/myapp # a directory (relative to cwd)
shell> pydeps myapp # ..the same, if you're in `src`
shell> pydeps pandas # an installed package, from anywhere
shell> pydeps pandas.core # ..or a module inside it`--find-package` saves some of that typing, and its help text hedges on purpose, saying it 'tries to automatically find the name of the current package'. Resolution order matters more than any of it. When a path and an importable name would both match, the path wins, so a run started in a directory that holds a pandas folder traces your working copy rather than the installed distribution.
One behaviour change is stated almost in passing. Package targets only worked as relative paths before v3.0.8, which meant standing in the directory containing the package. The examples above assume the newer behaviour, where a bare name resolves from anywhere.
Cluster flags and stdlib switches decide the edges
Standard library nodes stay out of the graph unless you ask for them, which keeps the default picture readable. `--pylib`, `--pylib-all`, `--include-missing` put them back: the first adds python std lib modules, the second adds all of them including C modules, and the third pulls in modules that are not installed or cannot be found on sys.path.
`--only MODULE_PATH` is the opposite lever, keeping just the modules whose paths start with a value you supply, and it repeats for several roots.
External dependencies get separate handling:
--externals create list of direct external dependencies
--cluster draw external dependencies as separate clusters
--min-cluster-size INT the minimum number of nodes a dependency must have before being clustered (default=0)
--max-cluster-size INT the maximum number of nodes a dependency can have before the cluster is collapsed to a single node (default=0)
--collapse-target-cluster collapse target module (this implies --cluster)`--externals`, `--cluster`, `--min-cluster-size INT`, `--max-cluster-size INT` bracket that group. The first produces a list of direct external dependencies, the second draws them as separate clusters, the lower limit decides how many nodes a dependency needs before being clustered at all, and the upper limit decides when the cluster collapses back into a single node. Both carry a default of 0, so neither threshold does anything until you feed it a real number.
The target itself has two treatments. `--keep-target-cluster` draws it as a cluster, and `--collapse-target-cluster` folds it into a single node, which also switches cluster mode on for you.
The bundled help text contradicts itself three times
The usage text quoted in the README disagrees with itself in three places, which matters if you ever wrap the tool in a script. `--show-cycles` is described as showing only import cycles and never appears in the synopsis printed above the option list.
`--start-color INT` does the opposite: it sits in the synopsis and then has no description at all in the list that follows.
`--no-output` is a third example, its entry ending with '(-t/-o will be ignored)' while the synopsis above offers no lowercase t at all, only the uppercase format flag. A reader trusting the synopsis alone would pass a switch that the option list never mentions.
--show-cycles show only import cycles
--version print pydeps version
--debug-mf INT set the ModuleFinder.debug flag to this value`--debug-mf INT` is worth reading first among the diagnostics, because it sets the ModuleFinder debug flag and is the clearest statement anywhere about how imports get collected in the first place.
`-v`, `-vv`, `-vvv`, `-L LOG`, `--debug` cover the rest of the diagnostics. The first three add verbosity, the log switch takes one of CRITICAL, ERROR, WARNING, INFO, DEBUG or NOTSET, and the last turns on every show and verbose option at once for debugging pydeps itself.
Config files that the README never explains
Configuration by file exists and is barely described. `--config FILE`, `--no-config` are the only two switches for it, and no default filename, format or lookup path is documented anywhere.
What the repository shows instead is a `.pydeps` file at the root, next to `.pre-commit-hooks.yaml`, `docker-images/`, `.devcontainer/`, `docs/`, and neither config-shaped file is unpacked for the reader. The container directories have no explanation either, even though a development container would have been the natural place to solve the dot-on-path problem.
`stdlib_list` is the entire runtime install declared in setup.py, and the file that pins the much larger development set is a different one:
PyYAML==6.0.3
stdlib-list>=0.12.0
tomlkit>=0.15.1A YAML reader and a TOML writer sit in those pins, consistent with a tool that can read either format, yet nothing states which extension the config flag expects or what its keys are called.
Read the root config file yourself and point `--config` at a copy before relying on any of it in a scripted build, because no filename is confirmed anywhere in the documentation.
The last push landed on 2026-10-01, the same day v3.0.9 was tagged, after v3.0.8 on 2026-09-07 and v3.0.7 on 2026-08-03.
Where pydeps stops being the right tool
Two things disqualify it for some codebases. `--max-bacon INT`, `--noise-level INT`, `--max-module-depth INT`, `--min-cluster-size INT` all change what reaches the picture, and none of them but the hop limit set to 0 brings back a full unfiltered view.
The second is the renderer, which is simply assumed present. The dot command has to be on the path, and no fallback is documented for a machine where it is not.
The documented alternative in approach is to stop one stage earlier and take the dot source instead of the picture:
--nodot, --no-dot skip dot conversion
--show-dot show output of dot conversion
--dot-output write dot code to file (instead of screen)`--nodot`, `--dot-output`, `--show-dot` are the handover points: skip the conversion, write the dot code to a file, or print it, and the drawing becomes someone else's job. That split is the one worth understanding, because pydeps is an import tracer first and a thin Graphviz wrapper second.
`--reverse`, `--rankdir`, `--rmprefix` reshape the picture without touching the trace. The first flips the arrows to point at imported modules instead of away from them, the second picks one of TB, BT, LR or RL with TB placing imported modules above the importing ones, and the third strips a prefix from displayed node names only.
On accuracy, the missing-module option shows that resolution is tied to sys.path, and the README does not document how imports built at runtime or hidden inside conditionals land in the graph. Treat any such picture as a claim to check against the code.
Version support is narrow and explicit: setup.py declares `python_requires=">=3.10"` and its classifiers list 3.10, 3.11, 3.12 and 3.13.
Adoption cost is otherwise low. setup.py declares BSD-2-Clause and the repository keeps a LICENSE file at the root, so downstream code inherits a permissive notice, and the project states no extra restriction of its own.
Editorial conclusion
pydeps earns a place on a codebase where import structure has grown tangled and two hops cannot explain it, provided you install Graphviz yourself and run `--max-bacon` with 0 at least once. It is the wrong tool when you need runtime behaviour rather than traced imports, or on a machine with no dot command on the path. Before adopting it, read the installed help for 3.0.9 against the README, because those two disagree, then check one real package with the noise level and the cluster sizes set deliberately.
Frequently asked questions
Does pydeps need Graphviz installed to draw a graph?
Yes, for anything that produces a picture. The README points at the Graphviz installation instructions and tells you to make sure the dot command is on your path. The `--nodot` switch is the documented way out when you only want the trace.
How do I make pydeps show more of the graph than the default two hops?
Raise the hop limit: `--max-bacon` excludes nodes more than n hops away, its default is 2, and 0 means infinite. Shape filters such as the noise level and the module depth option also remove nodes from the picture.
Can pydeps analyse a package that is not installed?
Yes, with `--include-missing`, which includes modules that are not installed or cannot be found on sys.path. The positional argument also takes a file, a directory or an importable name, and when a path and a name would both match, the path wins.
Does pydeps draw Python standard library modules by default?
Standard library modules stay out of the default picture. `--pylib`, `--pylib-all` add them back, the second including C modules, and `--only` narrows the graph to module paths starting with a value you supply.
Can pydeps give me the dot source instead of an image file?
Yes: `--show-dot`, `--dot-output`, `--nodot` print the dot code, write it to a file, and skip the conversion entirely, which leaves the drawing to whatever renderer you prefer.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/thebjorn-pydeps)Was this useful?