CLI tool
tonybaloney/wily avatar
tonybaloney/wily

wily: tracking complexity across every commit in a Python project

A Python application for tracking, reporting on timing and complexity in Python code

1,333 stars71 forksPythonApache-2.0

At a glance

What is it?
wily is a command line tool that walks the git history of a Python project, runs complexity and code analysis metrics over each revision, and stores the results in a cache so you can report on a metric over time, rank files by it, or diff the current tree against a chosen revision. The version line sits at a 2.0.0 alpha and the engine has moved from Python to a Rust extension.
Who is it for?
wily suits a team that has noticed complexity creeping up in a long lived Python codebase and wants evidence rather than instinct, since a per commit metric history is the only way to tell a refactor from a regression. It is the wrong choice for a project with a short history or a small codebase, because building the cache means running the analysis over every commit, and a poor fit if you need metrics on a language other than Python or on a codebase outside git.
Can I use it commercially?
Yes. Apache-2.0 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 16 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 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What it does: metrics per commit, cached, and diffable

wily is a command line application for tracking and reporting on the complexity of Python tests and applications, and the mechanism is git rather than a static analyser. It uses git to go through each revision, a commit, in a branch and run complexity and code analysis metrics over the code, so you can use it to limit your code or report on trends for complexity and length. The output is a cache, the .wily directory, that every other command reads. That is the design decision that matters. A linter tells you where the complexity is now, and wily tells you where it was five commits ago, which is the only version of that question that can tell you whether a change made things better. The project is Apache-2.0 licensed, the primary language is Python, and the last push to the repository was on 2026-10-07.

Installation, and the two distribution channels

wily can be installed via pip from Python 3.10 and above, with pip install wily, and alternatively the packages are available on conda-forge with conda install -c conda-forge wily. It is also usable as a pre-commit hook, and the documentation for that is the clearest statement of the intended workflow. You add a local repository to .pre-commit-config.yaml with a hook whose entry is wily diff, marked verbose, with the language set to python and wily in the additional dependencies. So the intended loop is that every commit gets measured against the previous one, and the complexity delta appears as part of the commit feedback rather than in a report someone remembers to generate. The tool is also described as usable in a CI/CD workflow to compare the complexity of the current files against a particular revision, and the pre-commit hook is the same mechanism pointed at the previous revision by default.

yaml
repos:
-   repo: local
    hooks:
    -   id: wily
        name: wily
        entry: wily diff
        verbose: true
        language: python
        additional_dependencies: [wily]

Five commands, and the one that has to run first

The command line usage section is five commands. wily build is the first step, building the wily cache with the statistics of your project, and it takes a target path, with a max-revisions option limiting how many historical commits to archive, an operators option selecting which metrics to compute and an archiver option that defaults to git if the project is a git repository and otherwise the filesystem. wily report shows a specific metric for a given file, printing the metric and the delta between each revision. wily rank shows the ranking for all files in a directory or a single file based on the metric provided, printing a table of files and their values. wily graph is similar to report but prints a graph in a browser instead of in the console. And wily index shows information about the build directory, printing the configuration and listing all the revisions analysed and the operators used. Everything except build requires that the .wily directory exists, so the order is not advisory.

console
$ wily build src/
$ wily diff src/ -r HEAD^1
$ wily diff src/ -r master

The metrics, grouped into the operators that compute them

wily list-metrics prints the metrics grouped by the operator that produces them, and the grouping tells you which analyses are cheap and which are not. The cyclomatic operator gives complexity, the Cyclomatic Complexity as a float with an AimLow direction. The maintainability operator gives rank, the Maintainability Ranking as a string with Informational direction, and mi, the Maintainability Index as a float with AimHigh. The raw operator counts lines: loc, lloc and sloc for lines, logical lines and source lines, plus comments, multi, blank and single_comments. The halstead operator gives the classic set, h1 and h2 for unique operators and operands, N1 and N2 for their counts, vocabulary, length, volume, difficulty and effort. And the cognitive operator gives cognitive_complexity. The direction markers are worth noting, because they tell you whether a rising metric is good news, and only the maintainability index and the comment counts go up when they should.

Configuration through wily.cfg, and the notebook options

You can put a wily.cfg file in your project directory and wily will override the runtime settings, and the --config flag overrides the path to the configuration on the command line. The options are an operators list choosing from cyclomatic, cognitive, maintainability, halstead and raw, an archiver defaulting to git, a path to analyse defaulting to the current directory, a max_revisions defaulting to 50 with the example setting 20, a cache_path override, and two notebook switches, include_ipynb to enable or disable scanning of Jupyter notebooks defaulting to true, and ipynb_cells to enable or disable reporting on individual notebook cells, also defaulting to true. The notebook handling is a real differentiator, since a data analysis project lives in .ipynb files and a tool that only reads .py would report almost nothing. The max_revisions default of 50 is also a practical constraint, because a project with years of history will not be measured end to end unless you raise it.

ini
[wily]
# list of operators, choose from cyclomatic, cognitive, maintainability, halstead and raw
operators = cyclomatic,raw
# archiver to use, defaults to git
archiver = git
# path to analyse, defaults to .
path = /path/to/target
# max revisions to archive, defaults to 50
max_revisions = 20
# override the default cache path
cache_path = /path/to/cache
# enable/disable scanning of Jupyter notebooks, defaults to true
include_ipynb = true
# enable/disable reporting on individual notebook cells, defaults to true
ipynb_cells = true

The build refuses a dirty tree, and the 2.0 line moves the engine to Rust

Two facts in the packaging metadata deserve attention. The first is a rule rather than a feature, that by default wily will assume your project folder is a git directory, and that wily will not build a cache if the working copy is dirty, meaning it has changed files not committed. That is a deliberate correctness choice, since the cache is keyed on commits and an uncommitted file has no revision to be measured against, and it will surprise you the first time you run it with work in progress. The second is the build backend. The project now builds with maturin using pyo3 bindings against a Rust manifest at backend/Cargo.toml, producing the module wily.backend, with the Python source alongside in src. The development status classifier is 3, Alpha, and the recent releases are 2.0.0a1 and 2.0.0a2 from April 2026, after 1.25.0 from October 2023, so the 2.0 line is a rewrite in progress rather than a settled release.

Dependencies, tooling and the shape of the repository

The dependency list is short and tells you what wily does not do. GitPython at 3.1.54 or newer handles the history, click is the command line, nbformat reads the notebooks, plotly draws the graphs and rich renders the console output. The metric computation itself is not a Python dependency, because it now lives in the Rust extension, which is the point of the maturin build. The toolchain is uv with a lock file, and the development groups cover pytest with coverage, radon, which is the Python complexity tool the metrics resemble, ruff for linting, safety for dependency checks and codespell. The Makefile shows the build and test loop, with maturin build for a release wheel, maturin develop for an editable install, ruff for linting and pytest for the suite, and a ci target that runs lint and test together. There are also gettext targets for extracting and compiling the message catalogues. The repository has src/, backend/, docs/, test/, a .devcontainer/, a .pre-commit-config.yaml and a .pre-commit-hooks.yaml.

Editorial conclusion

wily suits a team that has noticed complexity creeping up in a long lived Python codebase and wants evidence rather than instinct, since a per commit metric history is the only way to tell a refactor from a regression. It is the wrong choice for a project with a short history or a small codebase, because building the cache means running the analysis over every commit, and a poor fit if you need metrics on a language other than Python or on a codebase outside git. Before adopting it, note that the cache build refuses to run on a dirty working copy, that the default limit is 50 revisions, and that the 2.0 line is an alpha with a rewritten Rust backend, so pinning to 1.25.0 is the safer choice for a pipeline that has to keep working.

Frequently asked questions

What does wily do?

It is a command line application for tracking and reporting on the complexity of Python code. It uses git to go through each revision in a branch and runs complexity and code analysis metrics over the code, so you can limit your code or report on trends for complexity and length, with the results cached for later reports, rankings and graphs.

How do I install wily?

With pip install wily from Python 3.10 and above, or with conda install -c conda-forge wily. It can also be installed as a pre-commit plugin by adding a local repository to .pre-commit-config.yaml with the entry wily diff and wily in the additional dependencies.

Which metrics does wily compute?

They are grouped by operator. The cyclomatic operator gives complexity, the maintainability operator gives rank and the Maintainability Index, the raw operator counts lines and comments, the halstead operator gives the vocabulary, length, volume, difficulty and effort measures, and the cognitive operator gives cognitive complexity. Each metric carries a direction, so a rising maintainability index is good and a rising cyclomatic complexity is bad.

Why does wily build refuse to run on my working copy?

By default wily assumes your project folder is a git directory, and it will not build a cache if the working copy is dirty, meaning it has changed files not committed. The cache is keyed on commits, so an uncommitted file has no revision to be measured against. The archive limit also defaults to 50 revisions and is configurable with max_revisions.

What are the main wily commands?

wily build builds the cache of statistics for the project, wily report prints a metric and the delta between each revision for a file, wily rank prints a table of files and their metric values, wily graph prints a graph in a browser rather than the console, and wily index prints the configuration and lists the revisions analysed. Everything except build requires that the .wily directory exists.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. Releases
  5. tonybaloney/wily on GitHub
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tonybaloney-wily.svg)](https://hysenlabs.com/projects/tonybaloney-wily)

Was this useful?