Skip to main content
Enable quality checks: skylos . --quality

Why Quality Metrics Matter

Complex code isn’t just hard to read—it’s statistically more likely to contain bugs. Research shows that functions with complexity above 10 have significantly higher defect rates. At 20+, the function is nearly impossible to test exhaustively.

Complexity 1-10

Low riskEasy to understand, test, and maintain

Complexity 11-20

Moderate riskConsider refactoring when touching this code

Complexity 21+

High riskLikely contains bugs, very hard to test

Cyclomatic Complexity (SKY-Q301)

Cyclomatic complexity counts the number of independent paths through a function. Each decision point adds a path.

What Adds Complexity

ConstructComplexity Added
if / elif+1 per branch
for / while+1
try / except+1 per handler
with+1
and / or+1 per operator
Ternary x if y else z+1
List/dict/set comprehension+1

Example

def process_order(order): 
    if not order.valid:
        return None
    
    if order.priority == 'high':
        if order.value > 1000:
            apply_discount(order)
        notify_manager(order)
    elif order.priority == 'medium':
        queue_order(order)
    
    for item in order.items:
        if item.backordered:
            handle_backorder(item)
        elif item.fragile: 
            mark_fragile(item)
    
    return order

How to Reduce Complexity

    # Before: Complexity 8
def process_order(order):
    if not order.valid:
        return None
    # ... 50 more lines of nested logic

# After: Complexity 3 each
def process_order(order):
    if not order.valid:
        return None
    handle_priority(order)
    process_items(order)
    return order

def handle_priority(order):
    # Isolated complexity
    pass

def process_items(order):
    # Isolated complexity
    pass

Nesting Depth (SKY-Q302)

Deep nesting forces readers to hold multiple conditions in working memory simultaneously.
# Depth 5 - Very hard to follow
def bad_example():
    ## first
    if condition1:    
        ## second 
        for item in items:
            ## third
            if condition2:
                ## forth
                try:
                    ## fifth (RIP)
                    if condition3:
                        process(item)
                except:
                    pass

Severity Thresholds

DepthSeverityAction
≤3OKNo issue
4-5MEDIUMConsider refactoring
6-8HIGHShould refactor
9+CRITICALMust refactor

Reducing Nesting

# Before: Depth 4
def process_users(users):
    for user in users:
        if user.active:
            if user.verified:
                if user.has_permission:
                    do_something(user)

# After: Depth 1 (using continue)
def process_users(users):
    for user in users:
        if not user.active:
            continue
        if not user.verified:
            continue
        if not user.has_permission:
            continue
        do_something(user)

Structure Rules

Function Length (SKY-Q303)

Long functions usually do too much. They’re hard to test and understand.
[tool.skylos]
max_lines = 50  # Default
Rule of thumb: If you can’t see the whole function on one screen, it’s probably too long.

Argument Count (SKY-Q304)

Functions with many parameters are hard to call correctly and often indicate a missing abstraction.
# Too many arguments
def create_user(name, email, age, address, phone, department, role, manager, start_date):
    pass

# Group into objects
@dataclass
class UserInfo:
    name: str
    email: str
    age: int

@dataclass  
class Employment:
    department: str
    role: str
    manager: str
    start_date: date

def create_user(info: UserInfo, employment: Employment):
    pass
[tool.skylos]
max_args = 5  # Default

Logic Rules

Mutable Default Arguments (SKY-L001)

One of Python’s most common gotchas:
# Bug: list is shared across all calls
def append_to(item, target=[]):
    target.append(item)
    return target

>>> append_to(1)
[1]
>>> append_to(2)
[1, 2]  # Unexpected! Same list.
# Fixed
def append_to(item, target=None):
    if target is None:
        target = []
    target.append(item)
    return target

Bare Except (SKY-L002)

Catching everything swallows KeyboardInterrupt and SystemExit:
# Too broad
try:
    risky()
except:  # Catches Ctrl+C, sys.exit(), everything
    pass

# Specific
try:
    risky()
except Exception:  # Excludes BaseException subclasses
    handle_error()

Identity Comparisons (SKY-L003)

Use is for singletons:
# Wrong
if value == None:
    pass

# Correct
if value is None:
    pass

Visual Dashboard

When you run Skylos with --quality, you get a clear breakdown:
────────────────────────── Quality Issues ──────────────────────────
 #   Type        Function              Detail                      Location
 1   Complexity  process_order         McCabe=18 (target ≤10)      orders.py:45
 2   Nesting     validate_input        Depth 6 (target ≤3)         validators.py:23
 3   Structure   generate_report       142 lines (target ≤50)      reports.py:10
 4   Logic       create_cache          Mutable default argument    cache.py:5

Tip: split helpers, add early returns, flatten branches.

Configuration

[tool.skylos]
complexity = 10    # McCabe complexity threshold
nesting = 3        # Max nesting depth
max_args = 5       # Max function arguments
max_lines = 50     # Max function length

# Ignore specific rules
ignore = ["SKY-L003"]  # Allow == None comparisons

Per-Language Overrides

TypeScript often has higher complexity in UI code:
[tool.skylos.languages.typescript]
complexity = 15
nesting = 4

Integrating Quality Gates

Block PRs that introduce complex code:
- name: Quality Gate
  run: skylos . --quality --gate
With configuration:
[tool.skylos.gate]
max_quality = 10  # Fail if >10 quality issues

Next Steps