Code Quality Metrics That Matter: Complexity and Churn (Flask Case Study) - NextGenBeing
Back to discoveries

Code Quality Metrics That Matter: Complexity and Churn (Flask Case Study)

Run complexity and churn analysis with radon, lizard and git on Flask, find real hotspots, and decide which code quality checks belong in CI.

Data Science Premium Content 9 min read
Bekzod Erkinov

Bekzod Erkinov

May 1, 2026 • 104 views
Size:
Height:
9 min read 4,009 words Focus mode: Eye care:

Listen to Article

Loading...
0:00 / 0:00
0:00 0:00
Low High
0% 100%
⏸ Paused ▶️ Now playing... Ready to play ✓ Finished

Code Quality Metrics That Matter: Complexity and Churn (Flask Case Study)

The decision rule, up front: gate cyclomatic complexity (CCN) at 15 on changed files in CI, report churn monthly, and review the top five complexity-times-relative-churn hotspots by hand. Below is a runnable analysis of Flask that produces the hotspot list, what the research does and doesn't support, and a CI step you can paste in. If you want the pipeline basics first, see our CI/CD pipelines guide; for the human side, code quality in production.

Which code quality metrics to measure first

Versions: radon 6.0.1, lizard 1.24.1, pandas 3.0.6, Python 3.12.10, Flask at commit d73fa1cd (2026-09-08). You need a full clone; churn comes from history. All commands below were run on Windows 11 in Git Bash, so radon and lizard print paths with backslashes (flask\src\flask\app.py). The CI step normalises them; on an ubuntu runner they are already forward slashes.

python -m venv v && ./v/bin/pip install radon lizard pandas   # Windows: v\Scripts\pip
git clone https://github.com/pallets/flask.git

Radon is Python-only. Lizard supports 29 languages, so use it if your backend isn't Python.

Cyclomatic complexity: how to measure it in Python

Cyclomatic complexity is, per radon's docs, the number of decisions in a block plus one. Radon ranks it A (1-5) through F (41+) (ranks).

$ radon cc flask/src/flask -n C -s
flask\src\flask\app.py
    M 1227:4 Flask.make_response - C (16)
    M 1105:4 Flask.url_for - C (13)
    M 633:4 Flask.run - C (12)
flask\src\flask\cli.py
    F 1061:0 routes_command - C (18)
    F 120:0 find_app_by_string - C (14)
    F 698:0 load_dotenv - C (13)
    F 41:0 find_best_app - C (12)
flask\src\flask\debughelpers.py
    F 124:0 explain_template_loading_attempts - C (13)
flask\src\flask\sansio\app.py
    M 605:4 App.add_url_rule - C (13)
flask\src\flask\sansio\blueprints.py
    M 273:4 Blueprint.register - D (23)

$ radon cc flask/src/flask -a | tail -2
411 blocks (classes, functions, methods) analyzed.
Average complexity: A (2.727493917274939)

$ lizard flask/src/flask -C 15 -w
flask/src/flask\sansio\blueprints.py:273: warning: register has 61 NLOC, 23 CCN, 470 token, 3 PARAM, 105 length, 0 ND
flask/src/flask\app.py:1227: warning: make_response has 65 NLOC, 16 CCN, 338 token, 2 PARAM, 141 length, 0 ND
flask/src/flask\cli.py:1061: warning: routes_command has 35 NLOC, 18 CCN, 305 token, 2 PARAM, 47 length, 0 ND

10 of 411 blocks score C or worse under radon (C starts at 11); lizard's default threshold of 15 flags 3. The threshold decides how many warnings you get, so pick one and keep it. The average (A) says nothing; read the tail.

Code churn analysis with git log

Churn is how much a file changes. analyze.py counts non-merge commits and added+deleted lines per file with git log --numstat --no-renames, and joins them to lizard's per-file CCN. Derived columns: rel_churn (changed lines divided by current NLOC, a rough relative churn) and fix_ratio (share of a file's commits whose subject matches fix/bug/regression, a rough proxy). The hotspot score is percentile rank of total CCN times percentile rank of relative churn, my own crude heuristic rather than a standard. I use relative churn because Nagappan and Ball found absolute churn weak (details below). The script also prints the absolute-commits variant (hot_abs) so you can compare. The hotspot idea comes from Adam Tornhill.

"""usage: python analyze.py REPO_PATH SINCE [SUBDIR]"""
import collections, re, subprocess, sys
import lizard
import pandas as pd
from radon.complexity import cc_visit

repo, since = sys.argv[1], sys.argv[2]
subdir = sys.argv[3] if len(sys.argv) > 3 else "."
FIX = re.compile(r"\b(fix|fixes|fixed|bug|regression)\b", re.I)

def git(*args):
    r = subprocess.run(["git", "-C", repo, *args], capture_output=True,
                       text=True, encoding="utf-8", check=True)
    return r.stdout

# churn: one pass over history; --no-renames keeps paths real
log = git("log", f"--since={since}", "--no-merges", "--no-renames",
          "--numstat", "--format=@@%H|%s", "--", subdir)
churn = collections.defaultdict(lambda: dict(commits=0, added=0, deleted=0, fix_commits=0))
commits, is_fix = 0, False
for line in log.splitlines():
    if line.startswith("@@"):
        commits += 1
        is_fix = bool(FIX.search(line.split("|", 1)[1]))
        continue
    parts = line.split("\t")
    if len(parts) == 3 and parts[0].isdigit() and parts[2].endswith(".py"):
        c = churn[parts[2]]
        c["commits"] += 1
        c["added"] += int(parts[0]); c["deleted"] += int(parts[1])
        c["fix_commits"] += is_fix

# complexity: current working tree
rows = []
for path, c in churn.items():
    try:
        with open(f"{repo}/{path}", encoding="utf-8") as fh:
            src = fh.read()
    except FileNotFoundError:
        continue  # deleted since; nothing to join
    info = lizard.analyze_file.analyze_source_code(path, src)
    ccn = [f.cyclomatic_complexity for f in info.function_list]
    rows.append(dict(file=path.removeprefix(subdir.rstrip("/") + "/"), nloc=info.nloc,
                     max_ccn=max(ccn, default=0), sum_ccn=sum(ccn),
                     radon_max=max((b.

Unlock Premium Content

You've read 30% of this article

What's in the full article

  • Complete step-by-step implementation guide
  • Working code examples you can copy-paste
  • Advanced techniques and pro tips
  • Common mistakes to avoid
  • Real-world examples and metrics

Join 10,000+ developers who love our premium content

Bekzod Erkinov

Bekzod Erkinov

Author

Founder of NextGenBeing. Software engineer working with Laravel, Python, and cloud infrastructure. Writes about patterns that actually hold up in production. Based in Tashkent, Uzbekistan.

Free guide

Get the AI-Assisted Developer's Field Guide

The workflow, prompts, and tools I use to ship faster with AI — free when you subscribe. Plus new deep-dives in your inbox. No spam, unsubscribe anytime.

Comments (0)

Please log in to leave a comment.

Log In

Related Articles

Don't miss the next deep dive

Get one well-researched tutorial in your inbox each week. No spam, unsubscribe anytime.