CQ012: CyclomaticComplexity
Overview
| Property | Value |
|---|---|
| ID | CQ012 |
| Name | CyclomaticComplexity |
| Group | code-quality |
| Severity | WARNING |
Description
Measures cyclomatic complexity per function and flags those exceeding a configurable threshold (default: 10).
Cyclomatic complexity counts the number of independent paths through a function. High complexity indicates:
- Difficult to understand: Too many branches to track mentally
- Hard to test: Each path requires separate test cases
- Bug-prone: Complex logic is more likely to contain errors
- Maintenance burden: Changes risk introducing regressions
This check uses AST parsing to count decision points without requiring external tools like radon.
What it checks
The check scans all Python files in the package (excluding hidden directories and __pycache__), calculates complexity for each function, and reports those exceeding the threshold:
- PASSED: All functions are within complexity limit
- FAILED: One or more functions exceed the limit
- NOT_APPLICABLE: Package directory not found
How complexity is calculated
Complexity starts at 1 and increases by 1 for each decision point:
| Construct | Complexity Added |
|---|---|
if / elif |
+1 each |
for / while loops |
+1 each |
except handler |
+1 each |
and / or |
+1 each |
Ternary expression (x if cond else y) |
+1 |
Comprehension if clause |
+1 each |
def example(x, y): # Base: 1
if x > 0: # +1 = 2
if y > 0: # +1 = 3
return x + y
elif y < 0: # +1 = 4
return x - y
for i in range(x): # +1 = 5
if i % 2 == 0: # +1 = 6
continue
return x if y else 0 # +1 = 7
# Total complexity: 7Complexity grades
| Complexity | Grade | Meaning |
|---|---|---|
| 1-5 | A | Simple, low risk |
| 6-10 | B | Moderate, acceptable |
| 11-20 | C | Complex, consider refactoring |
| 21-30 | D | Very complex, high risk |
| 31+ | F | Untestable, needs rewrite |
How to fix
Replace conditionals with polymorphism
# Before - complexity 5
def calculate_price(product_type, base_price):
if product_type == "digital":
return base_price * 0.9
elif product_type == "physical":
return base_price + shipping_cost()
elif product_type == "subscription":
return base_price / 12
elif product_type == "bundle":
return base_price * 0.75
return base_price
# After - complexity 1 per method
class PriceCalculator:
def calculate(self, base_price):
return base_price
class DigitalPriceCalculator(PriceCalculator):
def calculate(self, base_price):
return base_price * 0.9
class PhysicalPriceCalculator(PriceCalculator):
def calculate(self, base_price):
return base_price + shipping_cost()Use strategy pattern with dictionaries
# Before - complexity 6
def handle_event(event_type, data):
if event_type == "click":
return handle_click(data)
elif event_type == "scroll":
return handle_scroll(data)
elif event_type == "hover":
return handle_hover(data)
elif event_type == "submit":
return handle_submit(data)
elif event_type == "load":
return handle_load(data)
return None
# After - complexity 2
EVENT_HANDLERS = {
"click": handle_click,
"scroll": handle_scroll,
"hover": handle_hover,
"submit": handle_submit,
"load": handle_load,
}
def handle_event(event_type, data):
handler = EVENT_HANDLERS.get(event_type)
if handler:
return handler(data)
return NoneExtract complex boolean logic
# Before - complexity 4 from boolean operators
def is_valid_user(user):
if (user.is_active and
user.email_verified and
not user.is_banned and
user.age >= 18):
return True
return False
# After - complexity 1
def is_valid_user(user):
return _meets_all_requirements(user)
def _meets_all_requirements(user):
return all([
user.is_active,
user.email_verified,
not user.is_banned,
user.age >= 18,
])Use guard clauses for early exit
# Before - deep nesting, complexity 5
def process_order(order):
if order:
if order.is_valid:
if order.payment_complete:
if order.in_stock:
ship_order(order)
return True
return False
# After - linear flow, complexity 5 but much clearer
def process_order(order):
if not order:
return False
if not order.is_valid:
return False
if not order.payment_complete:
return False
if not order.in_stock:
return False
ship_order(order)
return TrueSplit into focused functions
# Before - one function doing everything
def process_data(data):
# Validation (complexity 4)
# Transformation (complexity 3)
# Storage (complexity 3)
# Total: 10+
# After - focused functions
def process_data(data):
validated = validate_data(data) # complexity 4
transformed = transform_data(validated) # complexity 3
return store_data(transformed) # complexity 3Configuration
Adjust complexity threshold
[tool.pycmdcheck.checks.CQ012]
max_complexity = 5 # Stricter limit for critical code[tool.pycmdcheck.checks.CQ012]
max_complexity = 15 # More lenientSkip this check
[tool.pycmdcheck]
skip = ["CQ012"]CLI
pycmdcheck --skip CQ012