Code Metrics in Python - comprehensive tool for computing various software metrics
—
Compound metric combining Halstead volume, cyclomatic complexity, and lines of code to determine overall code maintainability. Uses the same formula as Visual Studio's maintainability index calculation to provide a single score indicating how easy code will be to maintain over time.
Primary functions for computing maintainability index from source code.
def mi_visit(code, multi):
"""
Visit code and compute Maintainability Index.
Analyzes Python source code to calculate a composite maintainability
score based on Halstead metrics, cyclomatic complexity, and code size.
Parameters:
- code (str): Python source code to analyze
- multi (bool): Whether to count multi-line strings as comments
Returns:
float: Maintainability Index score (typically 0-100+)
"""
def mi_compute(halstead_volume, complexity, sloc, comments):
"""
Compute Maintainability Index from component metrics.
Uses the standard MI formula:
MI = max(0, (171 - 5.2 * ln(V) - 0.23 * G - 16.2 * ln(L)) * 100 / 171)
Where:
- V = Halstead Volume
- G = Cyclomatic Complexity
- L = Source Lines of Code
Parameters:
- halstead_volume (float): Halstead volume metric
- complexity (float): Average cyclomatic complexity
- sloc (int): Source lines of code
- comments (int): Number of comment lines
Returns:
float: Maintainability Index score
"""
def mi_parameters(code, count_multi=True):
"""
Get all parameters needed for MI computation from source code.
Convenience function that extracts Halstead volume, cyclomatic
complexity, SLOC, and comment counts in one call.
Parameters:
- code (str): Python source code to analyze
- count_multi (bool): Whether to count multi-line strings as comments
Returns:
tuple: (halstead_volume, avg_complexity, sloc, comments)
"""
def mi_rank(score):
"""
Rank Maintainability Index score with letter grades.
Ranking system:
- A: Score > 19 (High maintainability)
- B: Score 9-19 (Medium maintainability)
- C: Score ≤ 9 (Low maintainability)
Parameters:
- score (float): Maintainability Index score
Returns:
str: Letter grade (A, B, or C)
"""The Maintainability Index uses a well-established formula that combines multiple code quality metrics:
MI = max(0, (171 - 5.2 × ln(HV) - 0.23 × CC - 16.2 × ln(LOC)) × 100 / 171)
Where:
from radon.metrics import mi_visit, mi_rank
# Well-structured code
good_code = '''
def calculate_average(numbers):
"""Calculate the average of a list of numbers."""
if not numbers:
return 0
return sum(numbers) / len(numbers)
def find_maximum(numbers):
"""Find the maximum value in a list."""
if not numbers:
return None
return max(numbers)
'''
# Poorly structured code
poor_code = '''
def complex_function(a,b,c,d,e,f):
if a>0:
if b>0:
if c>0:
if d>0:
if e>0:
if f>0:
return a+b*c/d-e+f
else:
return a+b*c/d-e
else:
return a+b*c/d
else:
return a+b*c
else:
return a+b
else:
return a
else:
return 0
'''
# Analyze maintainability
good_mi = mi_visit(good_code, multi=True)
poor_mi = mi_visit(poor_code, multi=True)
print(f"Well-structured code MI: {good_mi:.2f} ({mi_rank(good_mi)})")
print(f"Poorly structured code MI: {poor_mi:.2f} ({mi_rank(poor_mi)})")
# Output:
# Well-structured code MI: 74.23 (A)
# Poorly structured code MI: 12.45 (B)from radon.metrics import mi_compute, mi_parameters, h_visit
from radon.complexity import cc_visit, average_complexity
from radon.raw import analyze
code = '''
def process_user_data(user_input, validation_rules):
"""
Process and validate user input data.
Args:
user_input: Dictionary containing user data
validation_rules: List of validation functions
Returns:
Processed and validated user data
"""
# Validate input format
if not isinstance(user_input, dict):
raise TypeError("User input must be a dictionary")
# Apply validation rules
for rule in validation_rules:
if not rule(user_input):
raise ValueError("Validation failed")
# Process the data
processed_data = {}
for key, value in user_input.items():
if isinstance(value, str):
processed_data[key] = value.strip().lower()
elif isinstance(value, (int, float)):
processed_data[key] = value
else:
processed_data[key] = str(value)
return processed_data
'''
# Method 1: Use convenience function
halstead_vol, avg_complexity, sloc, comments = mi_parameters(code)
mi_score = mi_compute(halstead_vol, avg_complexity, sloc, comments)
print(f"Using mi_parameters:")
print(f" Halstead Volume: {halstead_vol:.2f}")
print(f" Average Complexity: {avg_complexity:.2f}")
print(f" SLOC: {sloc}")
print(f" Comments: {comments}")
print(f" MI Score: {mi_score:.2f}")
# Method 2: Manual calculation
halstead = h_visit(code)
complexity_blocks = cc_visit(code)
raw_metrics = analyze(code)
manual_mi = mi_compute(
halstead.total.volume,
average_complexity(complexity_blocks),
raw_metrics.sloc,
raw_metrics.comments
)
print(f"\nManual calculation:")
print(f" MI Score: {manual_mi:.2f}")
print(f" Rank: {mi_rank(manual_mi)}")from radon.metrics import mi_visit, mi_rank
# Original implementation
original = '''
def search_items(items, query, case_sensitive=False):
results = []
for item in items:
if case_sensitive:
if query in item:
results.append(item)
else:
if query.lower() in item.lower():
results.append(item)
return results
'''
# Refactored implementation
refactored = '''
def search_items(items, query, case_sensitive=False):
"""
Search for items containing the query string.
Args:
items: List of strings to search
query: Search query string
case_sensitive: Whether search should be case sensitive
Returns:
List of matching items
"""
if not case_sensitive:
query = query.lower()
return [item for item in items if query in item.lower()]
return [item for item in items if query in item]
'''
original_mi = mi_visit(original, multi=True)
refactored_mi = mi_visit(refactored, multi=True)
print(f"Original implementation:")
print(f" MI: {original_mi:.2f} ({mi_rank(original_mi)})")
print(f"Refactored implementation:")
print(f" MI: {refactored_mi:.2f} ({mi_rank(refactored_mi)})")
print(f"Improvement: {refactored_mi - original_mi:.2f} points")from radon.metrics import mi_visit
code_with_docstrings = '''
def data_processor(data):
"""
Process input data with comprehensive validation and transformation.
This function handles various data types and applies necessary
transformations to ensure data quality and consistency.
Args:
data: Input data in various formats
Returns:
Processed and validated data
Raises:
ValueError: If data format is invalid
TypeError: If data type is unsupported
"""
if not data:
return None
return str(data).strip()
'''
# Compare counting docstrings as comments vs not
mi_with_comments = mi_visit(code_with_docstrings, multi=True)
mi_without_comments = mi_visit(code_with_docstrings, multi=False)
print(f"Counting docstrings as comments: {mi_with_comments:.2f}")
print(f"Not counting docstrings as comments: {mi_without_comments:.2f}")
print(f"Difference: {mi_with_comments - mi_without_comments:.2f}")Higher Halstead volume (more complex code) decreases MI:
from radon.metrics import mi_visit
simple = "def add(a, b): return a + b"
complex_ops = """
def calculate(a, b, c, d):
return ((a + b) * c / d) ** 2 - (a - b) % c + (c & d) | (a ^ b)
"""
print(f"Simple function MI: {mi_visit(simple, True):.2f}")
print(f"Complex operations MI: {mi_visit(complex_ops, True):.2f}")Higher complexity decreases MI:
simple_logic = '''
def process(x):
return x * 2
'''
complex_logic = '''
def process(x):
if x < 0:
if x < -10:
return x * -1
else:
return 0
elif x > 0:
if x > 10:
return x * 2
else:
return x
else:
return 1
'''
print(f"Simple logic MI: {mi_visit(simple_logic, True):.2f}")
print(f"Complex logic MI: {mi_visit(complex_logic, True):.2f}")More lines of code decrease MI:
concise = '''
def factorial(n):
return 1 if n <= 1 else n * factorial(n - 1)
'''
verbose = '''
def factorial(n):
if n <= 1:
result = 1
else:
result = n
for i in range(n - 1, 1, -1):
result = result * i
return result
'''
print(f"Concise implementation MI: {mi_visit(concise, True):.2f}")
print(f"Verbose implementation MI: {mi_visit(verbose, True):.2f}")MI calculation handles various edge cases:
Maintainability Index integrates with radon's command-line interface:
# Command-line equivalent of mi_visit()
radon mi path/to/code.py
# Show actual MI values
radon mi --show path/to/code.py
# Filter by MI threshold
radon mi --min B --max A path/to/code.py
# JSON output for programmatic processing
radon mi --json path/to/code.pyInstall with Tessl CLI
npx tessl i tessl/pypi-radon