Avoid Artifacts with High Cyclomatic Complexity (Python): CAST Software Issues

Content verified by Anycode AI
September 13, 2024
Learn how to manage high cyclomatic complexity in Python using CAST AIP, enhancing code readability, maintainability, and testability through effective refactoring techniques.

Cyclomatic complexity is counted as a very useful metric in software engineering for computing the complexity of control flow in programs. Roughly speaking, if anything, it is a metric describing the quantity of linearly independent paths flowing through something; in other words, code that is problematic for reasoning, testing, and modifying.
 

This article will review what high cyclomatic complexity means, how to detect and handle it using the CAST AIP tool, and best practices for readable and maintainable Python code.
 

Understanding Cyclomatic Complexity
It was McCabe who, in 1976, introduced the so-called cyclomatic complexity - a metric that counts the number of linearly independent paths through other sources of a program. In other words, any given metric is computed by means of a program control flow graph: nodes correspond to blocks of code, while edges correspond to control flow paths between such blocks.
 

In Python, the following constructs contribute to the cyclomatic complexity: if, for, while, try, except, and function definitions. The more decision points a function has, the higher its cyclomatic complexity.
 

Calculation of Cyclomatic Complexity:
For a function f with:

  • E as the number of edges in the control flow graph,
  • N as the number of nodes,
  • P as the number of connected components (which is usually 1 for a single function),

The cyclomatic complexity M is calculated as:
M=E−N+2PM = E - N + 2PM=E−N+2P
Alternatively, it can be approximated by counting the number of decision points in the code plus one.
 

Example of Cyclomatic Complexity Calculation:

def example_function(x):
    if x > 0:
        print("Positive")
    elif x < 0:
        print("Negative")
    else:
        print("Zero")

In this example:

  • Number of decision points (if, elif, else): 2
  • Cyclomatic complexity (M): 2 + 1 = 3

 
Implications of High Cyclomatic Complexity
High cyclomatic complexity in Python functions or methods can have several negative consequences:
 

  • Low Readability: Complex functions are filled with many decision points and nested logic, hence complex to read and understand. It increases code review time and makes the occurrence of bugs probable.
     

  • Difficult to Test: The greater the cyclomatic complexity, the more test cases are required to achieve comprehensive test coverage. A function like this would require at least 10 different test cases to cover all the possible paths, which is quite cumbersome and time-consuming to implement and maintain.
     

  • More Costly Maintenance: The more complex the functionality is, the harder it is to adapt or extend without introducing new bugs. Developers may need more time to understand the code and its potential side effects, leading to increased maintenance costs.

 

  • Higher Risk of Bugs: Functions with many decision points are more prone to errors because each decision increases the likelihood of an incorrect branch being executed. High cyclomatic complexity can hide subtle bugs that are hard to detect through testing alone.
     

Identifying High Cyclomatic Complexity with CAST AIP
CAST AIP is a software analysis tool that can provide the necessary information related to code artifacts containing high cyclomatic complexity. This would allow developers to focus their refactoring efforts on such locations. CAST AIP can detect functions and methods whose cyclomatic complexity exceeds recommended thresholds by analyzing the control flow of Python code.
 

  • Description: CAST AIP analyzes Python source code to compute cyclomatic complexity and identifies functions or methods with high complexity. These artifacts are marked as issues that need refactoring to enhance the quality and maintainability of code.
     

  • Rationale: The high cyclomatic complexity of their application has been maintained for reasons of improving code readability by lessening the possibility of bugs, performing appropriate testing of the software in order to lessen maintenance cost. Thus, complex code is easier to understand, test, and modify; hence, it provides more robust applications that are easy to maintain.
     

  • Remediation: High cyclomatic complexity can be remediated by refactoring complex functions into smaller, more focused functions, removing branches that are not needed, or using polymorphism instead of complex conditional logic.

 

Code Examples: High Cyclomatic Complexity and Refactoring Strategies
Here are some examples to illustrate high cyclomatic complexity in Python code and how to refactor it for better maintainability.
 

Example 1: High Cyclomatic Complexity in a Function

def process_data(data):
    if data is None:
        print("No data provided")
        return
    if isinstance(data, list):
        if len(data) == 0:
            print("Empty list")
        else:
            for item in data:
                if isinstance(item, int):
                    print(f"Integer: {item}")
                elif isinstance(item, str):
                    print(f"String: {item}")
                else:
                    print("Unknown type")
    elif isinstance(data, dict):
        if not data:
            print("Empty dictionary")
        else:
            for key, value in data.items():
                if isinstance(value, int):
                    print(f"{key}: Integer {value}")
                elif isinstance(value, str):
                    print(f"{key}: String {value}")
                else:
                    print(f"{key}: Unknown type")
    else:
        print("Unsupported data type")

 

Cyclomatic Complexity Analysis:

  • Decision Points: Multiple if, elif, and nested conditions.
  • Cyclomatic Complexity: 11
     

Problems with High Complexity:

  • The function has many decision points and nested logic, making it hard to read and understand.
  • High complexity increases the number of test cases required to cover all paths.
     

Refactoring to Reduce Complexity:

def process_data(data):
    if data is None:
        print("No data provided")
        return


    if isinstance(data, list):
        process_list(data)
    elif isinstance(data, dict):
        process_dict(data)
    else:
        print("Unsupported data type")


def process_list(data):
    if not data:
        print("Empty list")
        return


    for item in data:
        print_item(item)


def process_dict(data):
    if not data:
        print("Empty dictionary")
        return


    for key, value in data.items():
        print_item(value, prefix=f"{key}: ")


def print_item(item, prefix=""):
    if isinstance(item, int):
        print(f"{prefix}Integer: {item}")
    elif isinstance(item, str):
        print(f"{prefix}String: {item}")
    else:
        print(f"{prefix}Unknown type")

 

Benefits of Refactoring:

  • Reduced Cyclomatic Complexity: Individual function complexity has been reduced, hence making it easier to understand and test.
  • Improved Readability: Because refactored code is modular by design, it becomes easier to read and comprehend as concerns are encapsulated within smaller more focused functions.
  • Improved Maintainability: Small functions are easier to modify and extend; thus, reducing the introduction of new bugs.
     

Example 2: Using Polymorphism to Reduce Complexity
High cyclomatic complexity often arises from complex conditional logic that can be replaced with polymorphism. Consider the following example:

class Shape:
    def draw(self):
        pass


def render_shape(shape):
    if isinstance(shape, Circle):
        print("Drawing a circle")
    elif isinstance(shape, Square):
        print("Drawing a square")
    elif isinstance(shape, Triangle):
        print("Drawing a triangle")
    else:
        print("Unknown shape")

 

Cyclomatic Complexity Analysis:

  • Decision Points: Multiple if-elif statements to handle different shapes.
  • Cyclomatic Complexity: 4
     

Refactoring Using Polymorphism:

class Shape:
    def draw(self):
        raise NotImplementedError("Subclasses should implement this!")


class Circle(Shape):
    def draw(self):
        print("Drawing a circle")


class Square(Shape):
    def draw(self):
        print("Drawing a square")


class Triangle(Shape):
    def draw(self):
        print("Drawing a triangle")


def render_shape(shape):
    shape.draw()  # Polymorphic call

 

Benefits of Refactoring:

  • Eliminated Conditional Logic: The render_shape function no longer contains any decision points, reducing cyclomatic complexity to 1.
  • Improved Extensibility: Adding new shapes only requires creating a new subclass with its own draw method, without modifying existing code.
  • Enhanced Readability: The code is cleaner and more intuitive, leveraging polymorphism for behavior-specific implementation.
     

Example 3: Removing Unnecessary Branches
Sometimes, high cyclomatic complexity is due to unnecessary or redundant branches that can be eliminated.

def compute_discount(price, discount):
    if discount < 0 or discount > 100:
        print("Invalid discount")
        return price


    if price <= 0:
        print("Invalid price")
        return 0


    return price * (1 - discount / 100)

 

Cyclomatic Complexity Analysis:

  • Decision Points: 2
  • Cyclomatic Complexity: 3
     

Simplified Function Without Redundant Checks:

def compute_discount(price, discount):
    if not (0 <= discount <= 100):
        raise ValueError("Discount must be between 0 and 100")


    if price <= 0:
        raise ValueError("Price must be greater than 0")


    return price * (1 - discount / 100)

 

Benefits of Refactoring:

  • Reduced Cyclomatic Complexity: The function still has a complexity of 3, but the branches are more logical and necessary, avoiding redundant checks.
  • More Robust Error Handling: Raising exceptions for invalid inputs ensures the caller can handle errors appropriately, improving robustness.
     

Best Practices to Reduce Cyclomatic Complexity
To effectively manage and reduce cyclomatic complexity in Python, consider the following best practices:
 

  • Refactor Long Functions: Break down long functions with high complexity into smaller, more manageable pieces. Each function should have a single responsibility.
     

  • Use Early Returns: Instead of using deeply nested if-else structures, use early returns to handle edge cases and exit the function early. This reduces nesting and improves readability.
     

  • Leverage Polymorphism: Replace complex conditional logic with polymorphism where appropriate. This simplifies code and reduces the need for multiple decision points.

 

  • Remove Redundant Logic: Eliminate unnecessary branches or checks in your code. Simplify conditions wherever possible to reduce complexity.
     

  • Adopt Defensive Programming: Validate inputs early and raise exceptions as needed to avoid complex error handling later in the code. This approach makes the code cleaner and reduces complexity.
     

  • Use Enumerations and Data Structures: In the presence of multiple states or types, it is generally better to use enumerations or data structures that map states to actions rather than chains of if-elif.

 

  • Perform regular code reviews with tools such as CAST AIP. Also perform static analysis, including the checking of high cyclomatic complexity and subsequent refactoring of code.
     

Conclusion
Following best practices for low cyclomatic complexity in Python development will result in maintainable, readable, testable applications. Large function decomposition, applying polymorphism, and making use of early returns are some of the best ways developers can reduce high-level complexity and provide higher-quality code. CAST AIP helps you find those areas where cyclomatic complexity is high and where further changes toward good practices will improve the robustness and maintainability of your Python codebase.

Improve your CAST Scores by 20% using the Anycode Security

Have any questions?
Alex (a person who's writing this 😄) and Anubis are happy to connect for a 10-minute Zoom call to demonstrate Anycode Security in action. (We're also developing an IDE Extension that works with GitHub Co-Pilot, and extremely excited to show you the Beta)
Get Beta Access
Anubis Watal
CTO at Anycode
Alex Hudym
CEO at Anycode