Skip to content

πŸ” Measuring Function Complexity in Python: Tools and Techniques ​

"Simple is better than complex. Complex is better than complicated." β€” The Zen of Python

Every Python developer has encountered that functionβ€”the one that spans hundreds of lines, with nested loops, multiple conditionals, and enough decision points to make your head spin. But how do you objectively determine when a function has become too complex?

In this post, we'll explore how to measure function complexity in Python using specialized libraries and tools, and how to interpret these metrics to improve your code quality.


What is Code Complexity? ​

Before diving into tools, let's understand what we're measuring. Cyclomatic complexity is the most common metric, developed by Thomas McCabe in 1976. It quantifies the number of linearly independent paths through a program's source code.

In simpler terms, it measures:

  • How many decisions (if/else, loops, etc.) exist in your code
  • How difficult your code is to test completely
  • The cognitive load required to understand the function

A function with higher complexity is typically:

  • More prone to bugs
  • Harder to maintain
  • More difficult to test thoroughly
  • More challenging to understand

πŸ“Š The Complexity Scale ​

Here's a general guideline for interpreting cyclomatic complexity scores:

Complexity ScoreRisk LevelInterpretation
1-5LowSimple function, easy to maintain
6-10ModerateModerately complex, consider refactoring
11-20HighComplex function, refactoring recommended
21+Very HighHighly complex, immediate refactoring needed

πŸ› οΈ Tools for Measuring Function Complexity ​

Let's explore some popular Python libraries that can help measure function complexity:

1. Radon: The Comprehensive Choice ​

Radon is a Python tool that computes various code metrics including cyclomatic complexity.

bash
# Install Radon
pip install radon

# Analyze a file
radon cc your_file.py

# Analyze a file with detailed output
radon cc your_file.py -s

Sample output:

your_file.py
    F 1:0 my_simple_function - A (2)
    F 10:0 my_complex_function - C (15)

The letters (A, B, C...) correspond to complexity risk levels, with A being the lowest risk and F being the highest.

2. McCabe: The Focused Choice ​

The McCabe package specifically targets cyclomatic complexity.

bash
# Install McCabe
pip install mccabe

# Create a Python script to use McCabe
python -m mccabe --min 5 your_file.py

This command will list functions with complexity of 5 or higher.

3. Wily: For Tracking Complexity Over Time ​

Wily is perfect for tracking how your code's complexity evolves over time:

bash
# Install Wily
pip install wily

# Build the Wily cache for your project
wily build your_directory/

# See complexity metrics
wily report your_file.py

# Track changes in complexity over time
wily graph your_file.py:my_complex_function -m cyclomatic

4. Flake8 with McCabe Plugin: For CI/CD Integration ​

If you're already using Flake8, you can integrate complexity checking:

bash
# Install Flake8 with McCabe plugin
pip install flake8

# Run Flake8 with complexity checking
flake8 --max-complexity 10 your_file.py

This flags functions with complexity higher than 10.


πŸ“ Hands-On: Identifying Complex Functions ​

Let's analyze a real Python function and measure its complexity:

python
def process_user_data(users, filters=None, sort_by=None):
    """Process user data with optional filtering and sorting."""
    result = []
    
    # Apply filters
    if filters:
        filtered_users = []
        for user in users:
            include_user = True
            for key, value in filters.items():
                if key in user:
                    if isinstance(value, list):
                        if user[key] not in value:
                            include_user = False
                            break
                    else:
                        if user[key] != value:
                            include_user = False
                            break
                else:
                    include_user = False
                    break
            
            if include_user:
                filtered_users.append(user)
    else:
        filtered_users = users.copy()
    
    # Apply sorting
    if sort_by:
        if isinstance(sort_by, str):
            filtered_users.sort(key=lambda x: x.get(sort_by, None))
        else:  # Assume it's a function
            filtered_users.sort(key=sort_by)
    
    return filtered_users

Let's measure this with Radon:

bash
$ radon cc -s example.py
example.py
    F 1:0 process_user_data - E (21)

A score of 21 indicates very high complexity! Let's refactor:

python
def filter_user_by_criteria(user, filters):
    """Check if a user matches all filter criteria."""
    for key, value in filters.items():
        if key not in user:
            return False
            
        if isinstance(value, list):
            if user[key] not in value:
                return False
        elif user[key] != value:
            return False
                
    return True

def process_user_data(users, filters=None, sort_by=None):
    """Process user data with optional filtering and sorting."""
    # Apply filters
    if filters:
        filtered_users = [user for user in users if filter_user_by_criteria(user, filters)]
    else:
        filtered_users = users.copy()
    
    # Apply sorting
    if sort_by:
        sort_key = sort_by if callable(sort_by) else lambda x: x.get(sort_by, None)
        filtered_users.sort(key=sort_key)
    
    return filtered_users

Now let's measure again:

bash
$ radon cc -s refactored.py
refactored.py
    F 1:0 filter_user_by_criteria - B (6)
    F 14:0 process_user_data - A (4)

Much better! We've reduced our main function's complexity from 21 to 4 by extracting a helper function.


πŸ”„ Integrating Complexity Checks into Your Workflow ​

For ongoing code quality, consider:

  1. Adding complexity checks to pre-commit hooks:
yaml
# .pre-commit-config.yaml
- repo: https://github.com/pycqa/flake8
  rev: 6.1.0
  hooks:
  - id: flake8
    args: ["--max-complexity=10"]
  1. Including complexity in CI/CD pipelines:
yaml
# GitHub Actions example
jobs:
  code-quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install radon
      - name: Check code complexity
        run: |
          radon cc --min C .
  1. Setting complexity thresholds in your IDE:

Most Python IDEs (like PyCharm) can be configured to highlight overly complex functions as you code.


🧠 Beyond Cyclomatic Complexity ​

While cyclomatic complexity is useful, consider these other metrics too:

  1. Cognitive Complexity: Similar but focuses on how hard it is for humans to understand
  2. Maintainability Index: A composite metric including complexity, lines of code, etc.
  3. Function Length: Simple but effective - functions over 50-100 lines are usually too complex

πŸ’‘ When to Refactor Complex Functions ​

High complexity doesn't always mean bad code. Consider refactoring when:

  1. The function has both high complexity AND high churn (frequently changed)
  2. You're having trouble understanding your own code
  3. Tests are difficult to write or becoming unwieldy
  4. New team members struggle to understand the function

Remember the rule of thumb: If it's hard to explain, it's probably hard to maintain.


🌟 Tips for Reducing Function Complexity ​

  1. Extract Helper Functions: Break large functions into smaller, focused ones
  2. Early Returns: Exit functions early for edge cases
  3. Polymorphism: Replace complex conditional logic with polymorphic classes
  4. Function Composition: Chain simple functions together instead of one complex one
  5. Remove Redundant Conditions: Simplify boolean logic where possible

πŸ“š See Also ​


πŸ” Final Thoughts ​

Measuring function complexity isn't just about chasing lower numbersβ€”it's about writing code that's easier to understand, test, and maintain. By incorporating complexity metrics into your development workflow, you can identify problematic functions before they become maintenance nightmares.

Remember that these tools are guides, not rules. Use your judgment when deciding what needs refactoring, and focus on making your code more maintainable rather than optimizing for metrics alone.

"Any fool can write code that a computer can understand. Good programmers write code that humans can understand." β€” Martin Fowler