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: 7

Complexity 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 None

Extract 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 True

Split 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 3

Configuration

Adjust complexity threshold

[tool.pycmdcheck.checks.CQ012]
max_complexity = 5  # Stricter limit for critical code
[tool.pycmdcheck.checks.CQ012]
max_complexity = 15  # More lenient

Skip this check

[tool.pycmdcheck]
skip = ["CQ012"]

CLI

pycmdcheck --skip CQ012

References