high-cyclomatic-complexity¶
Category: Complexity
Severity: Configurable by threshold
Triggered by: pyscn analyze, pyscn check
What it does¶
Flags execution scopes whose McCabe cyclomatic complexity exceeds the configured threshold. Modules, functions, methods, and executable class suites own independent control flow. Each if, elif, for, while, except, match case, and boolean clause inside a comprehension adds one to the count. A straight-line scope starts at 1.
pyscn does not count and / or short-circuit operators as separate branches.
Why is this a problem?¶
A high branch count means:
- More paths to read — every reviewer has to mentally simulate each branch to understand what the function does.
- More paths to test — full branch coverage requires one test per path; most highly-branched functions are under-tested in practice.
- Higher defect density — empirical studies since McCabe (1976) correlate complexity with bug rate.
- Harder to change safely — a small edit in one branch can silently break another.
Functions above ~10 are usually doing several jobs that could be named and separated.
Example¶
def price_for(user, cart, coupon, region):
total = 0
for item in cart:
if item.category == "book":
if region == "EU":
total += item.price * 0.95
elif region == "US":
total += item.price
else:
total += item.price * 1.10
elif item.category == "food":
if user.is_student:
total += item.price * 0.90
else:
total += item.price
else:
total += item.price
if coupon:
if coupon.kind == "percent":
total *= 1 - coupon.value
elif coupon.kind == "fixed":
total -= coupon.value
if total < 0:
total = 0
return total
Cyclomatic complexity: 13.
Use instead¶
Extract the per-item pricing and coupon handling, and replace the nested conditional with a dispatch table.
REGION_BOOK_MULTIPLIER = {"EU": 0.95, "US": 1.00}
def _book_price(item, region):
return item.price * REGION_BOOK_MULTIPLIER.get(region, 1.10)
def _food_price(item, user):
return item.price * (0.90 if user.is_student else 1.00)
PRICERS = {"book": _book_price, "food": _food_price}
def _item_price(item, user, region):
pricer = PRICERS.get(item.category)
return pricer(item, region) if item.category == "book" else \
pricer(item, user) if item.category == "food" else \
item.price
def _apply_coupon(total, coupon):
if coupon is None:
return total
if coupon.kind == "percent":
return total * (1 - coupon.value)
return total - coupon.value
def price_for(user, cart, coupon, region):
subtotal = sum(_item_price(i, user, region) for i in cart)
return max(0, _apply_coupon(subtotal, coupon))
Each helper now has complexity 1–3 and a single responsibility. Guard clauses (if coupon is None: return) flatten the remaining branches.
Options¶
| Option | Default | Description |
|---|---|---|
complexity.max_complexity |
0 |
Hard limit enforced by pyscn check. 0 means no enforcement in analyze; pyscn check --max-complexity uses 10 if unset. |
complexity.low_threshold |
9 |
Functions at or below this are reported as low risk. |
complexity.medium_threshold |
19 |
Above this, an execution scope is high risk. |
complexity.min_complexity |
1 |
Functions below this value are omitted from the report. |
Related metrics¶
pyscn computes two more complexity-flavored measurements alongside the McCabe count. They appear in the HTML report and JSON output but are not enforced by complexity.max_complexity:
- Cognitive Complexity (SonarQube-style). Penalizes nesting and breaks in linear flow more heavily than McCabe — useful for spotting functions that feel hard to follow even when their cyclomatic count is moderate. Surfaces as
CognitiveComplexityper function in the JSON output. - Raw code metrics per file (SLOC, LLOC, comment lines, blank lines, comment density). See
raw_metricsin the schema reference.
References¶
- McCabe, T. J. A Complexity Measure. IEEE Transactions on Software Engineering, 1976.
- Control-flow graph construction and cyclomatic counting:
internal/analyzer/complexity.go,internal/analyzer/complexity_analyzer.go,internal/analyzer/cfg_builder.go. - Rule catalog · too-many-constructor-parameters