Skip to content

Comparison with Ruff

Overview

Both Ruff and complexipy help improve Python code quality, but they measure different aspects of complexity and serve complementary purposes. Ruff's PLR0912 measures structural complexity, testing, and branch density, while complexipy measures how difficult code is for humans to understand and maintain.

Ruff's PLR0912: Too Many Branches

Ruff's PLR0912 (too-many-branches) rule is based on cyclomatic complexity and counts the number of decision points in a function.

How Ruff Counts Branches

Ruff counts each of these as a branch: - if, elif, else statements - for and while loops - and, or boolean operators - except clauses - Pattern matching cases

The default threshold is typically 12 branches per function.

Example: Ruff Analysis

def example(a, b, c, d):
    if a:           # Branch 1
        return 1
    elif b:         # Branch 2
        return 2
    elif c:         # Branch 3
        return 3
    elif d:         # Branch 4
        return 4
    else:           # Branch 5
        return 0
# Ruff: 5 branches

complexipy: Cognitive Complexity

complexipy implements cognitive complexity, which weights branches by their nesting level to reflect human understanding.

How complexipy Scores

complexipy uses the formula: complexity = base_score + nesting_level

def example(a, b, c, d):
    if a:           # +1 (1 + 0 nesting)
        return 1
    elif b:         # +1 (elif counts as +1)
        return 2
    elif c:         # +1
        return 3
    elif d:         # +1
        return 4
    else:           # +0 (else doesn't add base score)
        return 0
# complexipy: 4 points

Key Differences

1. Nesting is Critical in complexipy

Ruff treats all branches equally:

# Ruff: 3 branches
def flat_logic(a, b, c):
    if a:           # Branch 1
        return 1
    if b:           # Branch 2
        return 2
    if c:           # Branch 3
        return 3

# Ruff: 3 branches (same as above)
def nested_logic(a, b, c):
    if a:           # Branch 1
        if b:       # Branch 2
            if c:   # Branch 3
                return 1

complexipy accounts for nesting:

# complexipy: 3 points
def flat_logic(a, b, c):
    if a:           # +1
        return 1
    if b:           # +1
        return 2
    if c:           # +1
        return 3

# complexipy: 6 points (much worse!)
def nested_logic(a, b, c):
    if a:           # +1 (1 + 0)
        if b:       # +2 (1 + 1)
            if c:   # +3 (1 + 2)
                return 1

2. else Clauses

Ruff: Counts else as a branch (+1)

complexipy: else doesn't add base complexity, only nesting penalty

# Ruff: 2 branches
# complexipy: 1 point
def check(value):
    if value > 0:   # Ruff: +1, complexipy: +1
        return "positive"
    else:           # Ruff: +1, complexipy: +0
        return "non-positive"

3. Boolean Operators

Both count boolean operators, but with different philosophies:

Ruff: Counts each and/or as a separate branch

complexipy: Counts operators and penalizes mixed operator types

# Ruff: 3 branches (if + and + or)
# complexipy: 3 points (1 for if, +2 for boolean ops)
def check(a, b, c):
    if a and b or c:
        return True

4. Match Statements (Python 3.10+)

Ruff: Counts each case as a branch

complexipy: Match itself adds no complexity, only nested content counts

# Ruff: 3 branches (match + 2 cases)
# complexipy: 2 points (only the nested ifs)
match value:        # Ruff: +1, complexipy: +0
    case 1:         # Ruff: +1, complexipy: +0
        if x:       # Ruff: +1, complexipy: +1
            pass
    case 2:         # Already counted by Ruff
        if y:       # complexipy: +1
            pass

Practical Comparison Example

Here's a real-world scenario showing how the two tools differ:

def process_payment(order):
    if order is None:                              # Ruff: 1, complexipy: 1
        return False

    if not order.is_valid():                       # Ruff: 2, complexipy: 2
        return False

    if order.payment_method == "credit_card":      # Ruff: 3, complexipy: 3
        if order.amount > 1000:                    # Ruff: 4, complexipy: 5 (1+1 nesting)
            if not verify_fraud_check(order):      # Ruff: 5, complexipy: 8 (1+2 nesting)
                return False
        return process_credit_card(order)
    elif order.payment_method == "paypal":         # Ruff: 6, complexipy: 4
        return process_paypal(order)
    else:                                           # Ruff: 7, complexipy: 4
        return False

# Final Scores:
# Ruff: 7 branches
# complexipy: 8 points

In this example: - Ruff flags the function for having 7 branches (over typical threshold) - complexipy gives it 8 points, with most complexity coming from the nested fraud check - complexipy better identifies that the deeply nested fraud check is the problematic part

Which Should You Use?

Use Ruff (PLR0912) When:

  • You need to measure structural complexity and branch density
  • You're designing test coverage strategies
  • You want to limit the absolute number of decision points
  • You're already using Ruff for other linting

Use complexipy When:

  • You want to identify code that's hard for humans to understand
  • You're focused on code readability and maintainability
  • Nesting and flow complexity are concerns in your codebase
  • You're conducting code reviews focused on comprehension

Ruff and complexipy are complementary:

# In your pre-commit or CI pipeline
ruff check . --select=PLR0912  # Catch functions with too many branches
complexipy . --max-complexity-allowed 15  # Catch deeply nested code

Ruff catches wide functions (many branches), while complexipy catches deep functions (heavy nesting). Together, they provide comprehensive complexity coverage.

Configuration Examples

Ruff Configuration

# pyproject.toml
[tool.ruff]
select = ["PLR0912"]

[tool.ruff.pylint]
max-branches = 12

complexipy Configuration

# pyproject.toml
[tool.complexipy]
max-complexity-allowed = 15

Using Both in CI

# .github/workflows/quality.yml
- name: Ruff Linting
  run: ruff check . --select=PLR0912

- name: Cognitive Complexity Check
  run: complexipy . --max-complexity-allowed 15

Migration Strategy

If you're already using Ruff and want to add complexipy:

  1. Baseline First: Run complexipy . --snapshot-create to capture current state
  2. Set Thresholds: Start with a higher threshold (e.g., 20) and lower it over time
  3. Fix New Code: Only fail CI on new violations
  4. Gradual Improvement: Refactor legacy code opportunistically

Summary

Feature Ruff PLR0912 complexipy
Based on Cyclomatic Complexity Cognitive Complexity
Counts nesting ❌ No ✅ Yes
else penalty ✅ Yes ❌ No (only nesting)
Boolean operators ✅ Yes ✅ Yes
match statements ✅ Yes Partial (content only)
Best for Structural, testing, branch density How difficult code is to understand
Threshold ~12 branches ~15 points
Performance Fast (Rust) Very fast (Rust)

The Bottom Line: Ruff's PLR0912 measures structural complexity and branch density (useful for testing and analysis), while complexipy measures how difficult code is for humans to understand and maintain by penalizing nesting and flow breaks. Both are valuable, and using them together provides the best coverage for code quality.

Further Reading