Cyclomatic Complexity: What It Is and How to Fix It
Summary
Cyclomatic complexity counts the independent execution paths through a function. The formula is CC = decision points + 1. Scores below 10 are fine; above 15 means refactoring is overdue. Guard clauses, extracted helper functions, and lookup tables are the fastest ways to bring the number down. Cognitive complexity measures the same problem from the readability angle and is worth tracking alongside it.
Cyclomatic complexity is a number that tells you how many independent execution paths exist through a piece of code. A function with one if statement has two paths: one where the condition is true, one where it is false. Every new if, while, for, or case adds another path to that count.
Data analysts, ops teams reviewing sprint metrics, and anyone who reads static analysis reports will encounter this number regularly. It shows up in pull request checks, code quality dashboards, and automated linting pipelines.
The metric was invented by Thomas McCabe in 1976 and remains the standard benchmark when engineers ask: "Is this function getting too hard to test?" The short version: keep it below 10. Above 15, you are in refactoring territory. Biscuit would have fetched a simpler function from the start.
The Formula Behind Cyclomatic Complexity
The full formula looks like this:
M = E - N + 2PWhere:
M is the cyclomatic complexity score
E is the number of edges in the control flow graph
N is the number of nodes in the control flow graph
P is the number of connected components (usually 1 per function)
For most cases, you do not need to draw the control flow graph. The shortcut is faster:
CC = Decision Points + 1Count every if, else if, while, for, &&, ||, case, or ternary operator in the function. Add 1. That is your cyclomatic complexity.
A function with zero branches scores 1 -- it is a straight line from start to finish. Add one if, and you are at 2. Add five more conditions and you are at 7, which is still manageable. Add ten more and now you have a problem.
The score directly maps to the minimum number of test cases required to cover every path. A function with CC = 10 needs at least 10 tests for full path coverage. That alone makes the number useful during sprint planning.
Calculating It by Hand: A Real Example
Take this Python function:
def process_order(order, user):
if not order:
return None
if not user.is_active:
return None
if order.total > user.credit_limit:
if not user.has_override:
return None
if order.requires_signature and not user.has_signed:
return None
return order.confirm()Walk through the decision points:
if not order= +1if not user.is_active= +1if order.total > user.credit_limit= +1if not user.has_override= +1 (nested inside the previous block)if order.requires_signature= +1and not user.has_signed= +1 (theandcounts as a decision point)
Total: 6 decision points + 1 = CC of 7.
Manageable right now, but accumulating fast. Add two more input-validation conditions and you are at 9. Add one sprint of new business rules and you hit 13 -- that is when code reviewers start drawing red circles.
The nested condition (if not user.has_override) is the quiet danger. Sequential conditions are easier to read; nested conditions multiply the paths exponentially.

What Your Score Actually Means
Thresholds differ slightly by organization and domain, but the standard ranges are:
CC 1-10 (Low): Acceptable for most codebases.
CC 11-15 (Moderate): Add peer review; consider splitting the function.
CC 16-20 (High): Refactor before adding new features.
CC 21+ (Critical): High defect probability; prioritize refactoring now.
For safety-critical systems (medical devices, aviation software, financial engines), many teams set the hard limit at 5 to 7. The NIST guidelines recommend no higher than 10 for functions in regulated software. Regular product code generally lives comfortably under 10.
One caveat worth noting: the score counts decisions, not difficulty. A switch statement with 20 simple string mappings scores 21 but takes five seconds to read. A 3-level nested block might score 4 and take five minutes to understand. The number is a signal, not a verdict.
Cyclomatic vs. Cognitive Complexity: The Real Difference
Cyclomatic complexity was designed to tell you how many test cases you need. It treats all decision points equally: one if counts the same whether it is nested three levels deep or sitting at the top of the function.
Cognitive complexity, introduced by SonarSource, measures something different: how hard the code actually is to read. It penalizes nesting more aggressively than sequential conditions, which better reflects how developers actually process code.
The practical difference:
# Version A: 3 sequential conditions -- CC = 4, Cognitive = 3
def check_a(x, y, z):
if x > 0:
return False
if y > 0:
return False
if z > 0:
return False
return True# Version B: 3 nested conditions -- CC = 4, Cognitive = 6
def check_b(x, y, z):
if x > 0:
if y > 0:
if z > 0:
return False
return TrueBoth score CC = 4. But Version B is harder to read -- you need to hold the outer condition in your head while parsing the inner ones. Cognitive complexity correctly flags Version B as more demanding.
Use cyclomatic complexity for test planning and defect risk assessment. Use cognitive complexity to spot code that will slow your team down during review. They answer different questions.

Complex Excel Formulas Have the Same Problem
This is where it gets relevant for spreadsheet workers.
A deeply nested IF formula has exactly the same structure as deeply nested code. Each condition adds a path, and each layer of nesting adds cognitive load. The formula below has four decision points, giving a CC equivalent of 5:
=IF(A2="Sales",IF(B2>10000,"Tier 1",IF(B2>5000,"Tier 2","Tier 3")),IF(A2="Support","Fixed","Other"))That is not terrible yet. But add one more tier or one more department and the formula becomes the kind of thing nobody wants to edit six months later -- including the person who wrote it.
The fix in Excel mirrors the fix in code: flatten the nesting. Use IFS() when you have multiple conditions returning different values from the same column:
=IFS(AND(A2="Sales",B2>10000),"Tier 1",AND(A2="Sales",B2>5000),"Tier 2",A2="Sales","Tier 3",A2="Support","Fixed",TRUE,"Other")Or better: extract the tier logic to a helper column and reference it in your main formula. Two simple formulas are easier to audit than one complex one. Voila ce que ca donne dans une vraie cellule: the complexity drops, the logic stays.
Five Ways to Lower Your Cyclomatic Complexity
1. Extract helper functions.
If a function validates input, transforms data, and writes to a database, it is doing three jobs. Split it into three functions. Each drops to a lower score and can be tested independently.
2. Use early returns (guard clauses).
Instead of nesting success logic inside multiple if blocks, reject the bad cases early and return. The happy path stays at the left margin.
# Before: nested -- harder to follow
def process(user):
if user:
if user.is_active:
if user.has_permission:
return do_work(user)
return None
# After: guard clauses -- easier to extend
def process(user):
if not user:
return None
if not user.is_active:
return None
if not user.has_permission:
return None
return do_work(user)Same cyclomatic complexity in both cases, but the guard-clause version has lower cognitive complexity and is easier to extend.
3. Simplify boolean expressions.
A long if (a and b and not c and d) condition adds multiple decision points and is hard to read. Extract it to a named variable: eligible = a and b and not c and d. The if eligible: line reads in plain English and the intent becomes clear.
4. Replace conditionals with lookup tables.
A long if/elif chain that maps values to outputs is a hidden lookup table. Replace it with a dictionary:
# Before: CC = 5 for 4 branches
def get_rate(category):
if category == "A":
return 0.05
elif category == "B":
return 0.10
elif category == "C":
return 0.15
else:
return 0.20
# After: CC = 1
RATES = {"A": 0.05, "B": 0.10, "C": 0.15}
def get_rate(category):
return RATES.get(category, 0.20)5. Split large functions by concern.
If a function runs past 25 lines, ask what it actually does. Functions handling multiple concerns accumulate decision points from all of them. Breaking them apart makes each piece easier to test and reuse.
Tools That Calculate It Automatically
You do not have to count by hand on every pull request. Several tools integrate directly into standard development workflows:
SonarQube / SonarCloud -- calculates cyclomatic and cognitive complexity per function on every pull request; sends alerts when thresholds are breached
Radon (Python) -- run
radon cc yourfile.py -sin the terminal to get a score and letter grade per functionESLint (JavaScript/TypeScript) -- the
complexityrule enforces a hard limit:"complexity": ["error", 10]CodeClimate -- tracks complexity trends across branches and pull requests, useful for monitoring drift over time
VS Code extensions -- tools like CodeMetrics show the complexity score inline next to function signatures as you type
A practical setup: enforce a warning at 10 and a hard error at 15. That lets you catch drift without blocking every pull request on a minor overage. Review anything above 10 in code review; automate the block at 15.