This tutorial demonstrates how to systematically reduce code complexity through 5 progressive versions of a user registration function.
| Version | Cyclomatic | Cognitive | Maintainability | Halstead Volume | Key Technique |
|---|---|---|---|---|---|
| V1 | ❌ 68 | ❌ 589 | ❌ 44.89 | ❌ 1567 | Initial bad code (deeply nested) |
| V2 | ❌ 68 | ❌ 62 | ❌ 49.52 | ❌ 1666 | Early returns (90% cognitive reduction!) |
| V3 | ✅ Pass | ✅ Pass | ❌ 49.13 | ❌ 1040 | Extract validation functions |
| V4 | ✅ Pass | ✅ Pass | ✅ Pass | ✅ Pass | Data classes & configuration |
| V5 | ✅ Pass | ✅ Pass | ❌ 45.56 | ✅ Pass | Enterprise patterns (slight trade-off) |
Technique: Replace nested if-else with guard clauses Impact: Massive 90% reduction in cognitive complexity!
# Bad: Deeply nested
if condition1:
if condition2:
if condition3:
do_something()
# Good: Early returns
if not condition1:
return error
if not condition2:
return error
if not condition3:
return error
do_something()Technique: Single Responsibility Principle Impact: Distributes complexity across multiple small functions
# Bad: Everything in one function
def register_user(username, password, email, ...):
# 200 lines of validation and logic
# Good: Separate concerns
def validate_username(username):
# 10 lines
def validate_password(password):
# 10 lines
def register_user(...):
# 20 lines coordinating the validatorsTechnique: Group related parameters Impact: Reduces parameter count and Halstead volume
# Bad: Too many parameters
def register_user(username, password, email, age, country, phone, address, city, state, zip, ...):
# Good: Logical grouping
@dataclass
class UserRegistrationData:
username: str
password: str
# ...
def register_user(data: UserRegistrationData):Technique: Extract magic numbers and repeated values Impact: Improves maintainability and reusability
# Bad: Magic numbers everywhere
if len(username) < 3 or len(username) > 20:
# Good: Named constants
MIN_USERNAME_LENGTH = 3
MAX_USERNAME_LENGTH = 20
if not MIN_USERNAME_LENGTH <= len(username) <= MAX_USERNAME_LENGTH:Technique: Dependency injection, interfaces, service layer Impact: Highly testable and extensible (with slight complexity trade-off)
- Validator classes for reusability
- Repository pattern for data access
- Service layer for business logic
- Each component is independently testable
Version 5 shows that sometimes adding proper architecture can slightly increase some metrics. This is a trade-off:
✅ Benefits of V5:
- Highly testable (can mock dependencies)
- Easy to extend (add new validators)
- Clear separation of concerns
- Production-ready patterns
- More files and classes
- Slightly lower maintainability index
- More abstraction layers
Key Insight: The goal isn't to minimize all metrics at any cost, but to find the right balance for your project's needs.
- Start with early returns - Biggest bang for your buck
- Extract functions when they do one clear thing - Improves testability
- Group related data - Reduces parameter lists
- Use constants for magic values - Improves maintainability
- Consider architecture based on project size - Don't over-engineer small scripts
- Measure progress - Use tools like antipasta to track improvements
- Know when to stop - Perfect metrics aren't always the goal
# Check metrics for all versions
antipasta metrics --files DEMOS/TUTORIAL/*.py
# See the progression
antipasta metrics --files DEMOS/TUTORIAL/01_user_management_v1.py
antipasta metrics --files DEMOS/TUTORIAL/02_user_management_v2.py
# ... and so onWhen you encounter high complexity in your code:
- First: Can you use early returns to reduce nesting?
- Second: Can you extract clear, single-purpose functions?
- Third: Can you group related parameters into objects?
- Fourth: Are there magic numbers to extract as constants?
- Finally: Does the code warrant a more sophisticated architecture?
Remember: The goal is maintainable, understandable code that passes reasonable complexity thresholds, not necessarily the lowest possible numbers.