Bekzod Erkinov
Listen to Article
Loading...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
Don't have an account? Start your free trial
Join 10,000+ developers who love our premium content
Keep reading
Mastering CI/CD Pipelines with Jenkins and Docker: A Deep Dive into Automated Deployment and Testing
14 min · 942 views
AI WorkflowsOrbit Determination Program (ODP) vs ORBIT14 vs ESOC: What Public Papers Say, Plus a Python Batch Fit
10 min · 266 views
Data ScienceDeep Dive into Next.js: Server-Side Rendering and Static Site Generation
32 min · 142 views
Bekzod Erkinov
AuthorFounder of NextGenBeing. Software engineer working with Laravel, Python, and cloud infrastructure. Writes about patterns that actually hold up in production. Based in Tashkent, Uzbekistan.
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