Simplify your Python Code: Automating Code Complexity Analysis with Wily | by Niels D. Goet | Towards Data Science

So you’ve written a piece of Python code and it does the job. Great, but is your code sufficiently simple? Complex code is difficult to read and makes code maintenance more costly. Catching complexity early can save time, money, and a lot of frustration. In this post, I’ll show you how to use the wily command-line tool to trace the complexity of your code over time.
Code complexity matters. Unnecessarily complex code is harder to read, and more difficult to maintain. If your code is hard to understand, it’s harder to spot existing bugs and easier to introduce new ones. Clunky code complicates teamwork, and makes it hard to get new colleagues up to speed. There’s a reason why tech companies have complexity thresholds that must be met.
Developers use several different measures of code complexity. Two such measures are universally represented in complexity analysis tools.
First, the McCabe’s Cyclometic Complexity (CYC) relies on graph theory. Developed by Thomas McCabe in 1976, the metric is calculated from a function’s control flow graph, which consists of nodes and edges.
Based on such a graph, we can calculate the CYC using the following formula:
CYC = E – N + 2P
In this formula, P is the number of predicate nodes (i.e. nodes that contain an if/else condition), E is the number of edges, and N is the number of nodes. CYC effectively is a measure of the number of independent paths through your module, giving some indication of how difficult the code is to understand, and how many unit tests would be required to achieve full test coverage (i.e. its “testability”).
The higher the value of the CYC, the more complex your code. The Software Engineering Institute at Carnegie Mellon defines the following ranges (see this publication, p. 147):
1–10: low risk, simple program;
11–20: moderate risk, more difficult program;
21–50: high risk, very difficult program;
> 50: very high risk, untestable program.
Second, the maintainability index (MI) is a combination of the McCabe metric (C), the Halstead Volume (V), and Lines of Code (LoC). In formula1:
MI = 171 - 5.2*ln(V) - 0.23*(C) - 16.2*ln(LoC)
The MI is bound between 0 and 100 in theory but not in practice (most software implementations cap it at 100). The original paper introducing the metric noted the following thresholds: if your code’s MI is below 65 is is hard to maintain; if it’s 85 or higher, your code is easy to maintain. Anything between 65 and 85 is moderately maintainable (Coleman, Lowther, and Oman, 1994). The rescaled version (to between 0 and 100) used by Visual Studio2 puts the thresholds at 0 to 9 for low maintainability, 10–19 for moderate, and 20 and above for high maintainability respectively. (Please note that different IDEs and libraries may use different thresholds.)
The CYC and MI should not be used in isolation to establish whether your code is overly complex or hard to maintain, for a number of reasons.
First, these metrics do not account for other indicators of unnecessary complexity such as code duplication, nesting depth, or readability to a human. Second, the MI in particular suffers from bunch of problems that make it inconsistent. It was calibrated several decades ago and makes some rather dubious assumptions on how increases and decreases in some of the variables it uses contribute to overall complexity. (For a more detailed assessment of the shortcomings of the MI, check out this excellent blog post by Arie van Deursen, professor at the Delft University of Technology.)
Finally, we should expect the MI’s values to differ depending on what framework you are analysing. A simple measure, like Lines of Code (LOC) may therefore be preferred.
These limitations do give us some idea of when and how to use the CYC and MI. First, the MI cannot reliably be used to compare the complexity of two different systems, even if they are built using the same frameworks. Second, we cannot use hard cutoffs for when a piece of code is simple and maintainable enough because these metrics cannot be divorced from human evaluation and expert knowledge of a code base.
Instead, the CYC and MI may be useful — in combination with other simpler metrics — if we treat them as heuristics that help us identify potential problems with our code. This can be particularly useful if we evaluate our code regularly, and observe how the metrics evolve over time. Sudden and large changes should catch our attention and prompt us to investigate further. Needless to say: as with most code quality tools, the CYC and MI should complement quality control in your development workflow, not replace it.
In the remainder of this post, I’ll show you how to integrate wily in your Python development workflow to perform complexity analyses for you at every new code commit. Wily is a command-line tool that allows you to analyse, track, and visualise the complexity and maintainability of your Python code. While the mccabe and radon libraries offer similar functionalities, wily has some nice features that allow us to trace complexity over time with relative ease. Check out the video below for an introduction to wily by its author, Anthony Shaw.
Detecting code complexity is only as useful as the frequency with which you perform the analysis. If you regularly check your code complexity, you can easily spot sudden increases. Such outliers are not necessarily problematic, but they should prompt you to investigate. The best way to ensure you get these insights frequently is to include wily in your project’s pre-commit hooks.
Pre-commit hooks are small “scripts” that are run locally on staged files when using the git commit command. (If you’d like to learn more about pre-commit hooks, you may be interested in my post on using pre-commit hooks to automatically enforce code style.)
To get started with pre-commit hooks, first install the pre-commit library using terminal (instructions for Homebrew and Conda are available on this page):
pip install pre-commit
You can check whether your installation was successful using the command below. This command should return the version of your pre-commit installation.
pre-commit --version
After you’ve installed pre-commit, create a .yaml file. Your .yaml file specifies your pre-commit hooks configuration. Use the following command in terminal to create a new .yamlfile in your project directory:
nano .pre-commit-config.yaml
Subsequently, add the following configuration:
repos:-   repo: local    hooks:    -   id: wily        name: wily        entry: wily diff        verbose: true        language: python        additional_dependencies: [wily]
Now that you have installed the pre-commit package and set up your .yamlfile, you’re all set to use pre-commit with wily to analyse code complexity. To get started, first run the following command in terminal to install the pre-commit hooks:
pre-commit install
After you’ve installed the pre-commit hooks, the pre-commit command will be executed every time you commit a change to git. Optionally, you can run the pre-commit hooks directly from terminal by using the following command:
pre-commit run
Before you can use wily with pre-commit, you’ll have to go run wily build from terminal. The build command compiles a cache of the complexity changes of your code in your last 50 commits. You need this cache so that wily can compute metrics across your commits. Alternatively, you can use wily setup and follow the instructions in terminal.
After you’ve set up and configured pre-commit and created your wily cache, the wily diff command will be run automatically upon every new commit. Upon a new commit, your terminal output will show the complexity of the version of the code in your current commit compared to the previous revision. The output below shows an example of wily’s diff report after removing some lines of code. We obtain four scores, including Lines of Code (LOC), Unique Operands, and the Cyclomatic Complexity and Maintainability Index. If your complexity increases dramatically after a relatively small change, you should probably pause and reconsider the changes you’ve just made.
Getting an overview of how your code changed between two commits is useful, but does not give us that much data to work with. To identify and prioritise potential problem areas, we have to consider how a specific module changed over time. To get this information, run the following commands in terminal:
wily buildwily report <my-module-name>
As shown in the example below, the report command will produce a CLI table of the changes in complexity metrics between all commits for the specified module. This report allows you to easily spot changes, as well as patterns and trends in complexity, and where you might have gone wrong.
Another useful command to identify (potentially) problematic modules within a larger project is wily rank, which will show the maintainability index of your different modules. In this case all values are above 75, meaning they’re easy to maintain. (Do note the caveats with using the maintainability index that I’ve outlined earlier in this post).
Finally, it’s easiest to spot trends when plotting the complexity of a piece of code across different commits. You can visualise a module’s complexity report using the graph command, e.g.:
wily graph create_tables.py loc sloc complexity
This command will produce a plot of the create_tables.py module (see below), showing the lines of code on the y-axis, the commits on the x-axis, and the complexity represented by the size of the bubbles on each observation.
Combined, wily’s diff, report, rank, and graph commands can give you useful information on whether your code is becoming unnecessarily complex.
Writing simple, clear, and legible code is challenging. While code style and (unit) testing can to a large degree be automated, the design of your code itself is still a human task. That is, if the latest advancements in machine programming do not make developers obsolete. Automating complexity analysis can help you make informed decisions about when to reconsider your code, and what areas of your code base to prioritise. These analyses can never replace a proper code design process, but it can go a long way to avoiding unnecessary mistakes and catching potential problems during the development process.
Thanks for reading!
1 Original version of the MI. Note that different versions of the MI exist. See, e.g. Radon’s documentation on this metric.
2 Visual Studio’s rescaled MI can be calculated using the following formula: MAX(0,(171–5.2*ln(V) — 0.23 * (C) — 16.2*ln(LoC))*100 / 171). See this article for further details.
Support my work: If you liked this article and you’d like to support my work, please consider becoming a paying Medium member via my referral page. The price of the subscription is the same if you sign up via my referral page, but I will receive part of your monthly membership fee.
Are you interested in learning more about automating your Python development process? Check out some of my earlier posts on this topic:
towardsdatascience.com
medium.com
Please read this disclaimer carefully before relying on any of the content in my articles on Medium.com.