A reviewer says this function is hard to follow and asks you to simplify it. It collects the items from orders in some regions, and skips blocked items:
from dataclasses import dataclass
@dataclass
class Order:
region: str
items: list[str]
def collect_nested(
orders: list[Order], regions: set[str], blocked: set[str]
) -> list[str]:
out: list[str] = []
for order in orders:
if order.region in regions:
for item in order.items:
if item not in blocked:
out.append(item)
return out
A complexity score puts a number on how hard a function is to follow. Two scores are common, and they count different things. This site does not measure either one in exercises, so treat them as tools for your own projects.
Cyclomatic complexity counts decisions
Cyclomatic complexity measures how many separate paths run through a
function. For a simple function, start at 1 and add 1 for each if, elif,
for and while. collect_nested has two loops and two if statements, so
it scores 1 + 4 = 5.
Ruff's C901 rule flags each function that scores above a limit. The
limit is 10 unless you set your own in pyproject.toml:
[tool.ruff.lint.mccabe]
max-complexity = 8
Here is an equivalent version that skips unwanted cases early:
def collect_flat(
orders: list[Order], regions: set[str], blocked: set[str]
) -> list[str]:
out: list[str] = []
for order in orders:
if order.region not in regions:
continue
for item in order.items:
if item in blocked:
continue
out.append(item)
return out
It still has two loops and two if statements, so it also scores 5. It nests
less, though. Every order that reaches the item loop is already in a wanted
region. Check that the two versions agree:
orders = [Order("west", ["tea", "coffee", "tea"]), Order("east", ["bread"])]
assert collect_nested(orders, {"west"}, {"coffee"}) == ["tea", "tea"]
assert collect_flat(orders, {"west"}, {"coffee"}) == ["tea", "tea"]
Cognitive complexity also counts nesting
Cognitive complexity comes from Sonar, the company behind the SonarQube code
checker. It adds points for nesting. Each if or loop adds 1, plus 1 for
each if or loop it sits inside. A continue adds nothing. Counted by hand
with Sonar's rules, the two versions score differently:
collect_nestedscores 1 + 2 + 3 + 4 = 10. The outer loop adds 1, theifinside it adds 2, the inner loop adds 3, and the innermostifadds 4.collect_flatscores 1 + 2 + 2 + 3 = 8, because its inner loop and theifinside that loop each sit one level shallower.
The lower score matches what a reader has to track. In the nested version,
out.append(item) runs inside two loops and two if statements. In the flat
version, it runs inside only the two loops, because each if has already
ended with continue.
Check your understanding
Moving the inner loop into a helper function lowers the score of collect_nested. Is the code now easier to follow?
Show answer
Hide answer
Only if the helper is an idea worth naming. The reader still has to understand
the helper, so the total work may not drop. A helper such as
allowed_items(order, blocked) helps, because its name says what it returns.
Splitting code into small, vaguely named functions to get under a limit makes
it harder to follow.
A score is not a test count
A score of 5 does not mean five tests are enough. A loop can run zero, one or many times, and the values you pass in create cases that no branch count shows. For these collectors, test these cases:
- an empty list of orders,
- an order from a region you did not ask for,
- an order with no items,
- a blocked item,
- a repeated item, which should appear twice,
- several accepted orders, to check that the output keeps their order.
Use the score to start a review
Run C901 as advice first, as the previous article showed, and read what it
flags. Fix confusing names, repeated conditions and deep nesting where the
change helps. A long, flat match that maps each command to one function call
can score high and still be easy to read. When a clear function goes over the
limit, keep it, and add # noqa: C901 to its def line with a comment that
says why.
Going further
Cyclomatic complexity comes from a 1976 paper by Thomas McCabe. That is why
Ruff puts the settings for C901 in a section named mccabe. Sonar's full rules for cognitive
complexity also count sequences of and and or, and recursion. Python tools
that compute cognitive complexity can count some constructs differently, so
check a tool's documentation before you compare its numbers with these.