Skip to content

Complexity Analysis with Radon

  • Cyclomatic complexity (branches/paths)
  • Maintainability index

High complexity often means:

  • harder to test
  • more bugs
  • slower changes
bash
radon cc -a your_package

Maintainability:

bash
radon mi your_package
  • identify hotspots
  • refactor into smaller functions
  • add tests around risky logic first

Every measurement on this page — and on the other five tools in this phase — comes from running the tool against this deliberately flawed file:

sample.py
import os
import subprocess
import hashlib
 
 
def process(items, user_input, flag = False):
    unused_var = 42
    result=[]
    for i in items:
        if i > 0:
            if flag:
                if i % 2 == 0:
                    result.append(i*2)
                else:
                    result.append(i)
            else:
                result.append(i)
    password = "hunter2"
    h = hashlib.md5(password.encode()).hexdigest()
    os.system("echo " + user_input)
    subprocess.call("ls " + user_input, shell=True)
    return result
 
 
def add(a: int, b: int) -> int:
    return a + b
 
 
x = add("1", 2)
toolwhat it reported on sample.py
flake85 style and dead-code issues. No security findings.
pylint4 issues, score 8.10/10
mypy1 type error, which neither linter saw
bandit5 security issues, 3 of them HIGH
radoncomplexity A (5), maintainability A (56.30)

The headline is that no tool subsumes another. flake8 read the whole file and reported nothing about the shell injection on line 20. bandit read the same file and said nothing about the type error on line 29. Running one and concluding the code is clean is the mistake this phase exists to prevent.

sketch Six tools, one file, almost no overlap p5.js
Each tool was run against the same 29-line file. Click a tool to see what it found and, more importantly, what it did not.

Every if, for, while, and, or, except and comprehension adds one path through a function. Radon counts them:

radon cc -s sample.py
F 6:0  process - A (5)
F 25:0 add     - A (1)

add has a single path. process has five, from one loop and three nested conditions. Measured on a deliberately tangled grading function:

radon cc -s messy.py
F 1:0 grade - C (15)
diagram Diagram mermaid
rankcomplexityreading
A1-5simple
B6-10fine
C11-20worth a look
D21-30hard to test
E / F31+rewrite it

The practical value is testing: a function with complexity 15 needs roughly 15 test cases to cover its paths. That is the honest cost of leaving it alone, and it is a better argument for splitting it than any appeal to elegance.

Maintainability index is a different number

Section titled “Maintainability index is a different number”
radon mi -s messy.py
messy.py - A (48.99)

The same file whose function scored C scored A for maintainability, because MI combines complexity with volume and lines of code — and the file is short. A small tangled function and a long simple one can land in the same place.

Read them together: MI for a file-level trend over time, CC to find the specific function worth splitting.

radon cc, before and after black
before:  F 1:0 grade - C (15)
after :  F 1:0 grade - C (15)

Measured. This is the most useful thing radon tells you about the rest of the toolchain: black makes code readable, radon measures whether it is simple, and those are different properties.

commands
radon cc -s -a myapp/           # per-function scores, plus the average
radon cc -s -nc myapp/          # only rank C and worse
radon mi -s myapp/              # maintainability per file
xenon --max-absolute C myapp/   # fail CI above a threshold

radon cc -nc is the one to run on an unfamiliar codebase: it lists only the functions that are actually complicated, which is usually a short and very informative list.

The usual fix is to extract the branches into named functions. The total complexity across the module barely changes; what changes is that each piece can be tested and named independently, and the top-level function reads as a description of the decision rather than an implementation of it.

pch.quizTag pch.quizDefaultTitle
  1. A function scores C (15) for cyclomatic complexity. What is the most concrete consequence?

    pch.quizShowAnswer

    B — it has about 15 independent paths, so covering it properly takes roughly 15 test cases — That testing cost is the honest argument for splitting it, and a better one than an appeal to elegance.

  2. The same file scored C for a function's complexity and A (48.99) for maintainability. How can both be true?

    pch.quizShowAnswer

    B — they measure different things: MI combines complexity with volume and line count, and the file is short — Use MI as a file-level trend and CC to find the specific function worth splitting. A small tangled function and a long simple one can score alike.

  3. Running black over the messy function left its complexity at C (15). What follows?

    pch.quizShowAnswer

    B — formatting and complexity are independent properties; readable is not the same as simple — Black removes an argument from code review. It does nothing about how many paths run through a function.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading