Skip to main content

The Problem: Static Analysis Has Limits

Static analysis reads your code without running it. This works great for direct calls:
def helper():
    return "I help"

def main():
    helper()  # ✅ Static analysis sees this call
But Python is dynamic. Code can call functions without naming them directly:
class Visitor(ast.NodeVisitor):
    def visit_FunctionDef(self, node):  # ❌ Flagged as unused!
        return self.generic_visit(node)

# Called via: getattr(self, 'visit_' + node.__class__.__name__)
visitor.visit(some_node)
Static analysis sees visitor.visit() but can’t know that visit_FunctionDef gets called internally via getattr().

The Solution: Runtime Call Tracing

The --trace flag runs your test suite with Python’s sys.settrace() enabled, recording every function that actually gets called.
skylos . --trace
This:
  1. Runs pytest with call tracing enabled
  2. Records every function call to .skylos_trace
  3. Runs static analysis
  4. Cross-references findings with trace data
  5. Removes false positives

What Gets Captured

PatternStatic AnalysisWith --trace
visitor.visit(node)visit_FunctionDef❌ Missed✅ Caught
getattr(obj, "method")()❌ Missed✅ Caught
globals()["func_name"]()❌ Missed✅ Caught
Plugin hooks (pytest_configure)❌ Missed✅ Caught
Signal handlers (@receiver)❌ Missed✅ Caught
Dynamic imports + calls❌ Missed✅ Caught

When to Use --trace

Use It

  • AST/CST visitor patterns
  • Plugin architectures
  • Heavy getattr() / reflection
  • Many false positives from static analysis
  • Projects with good test coverage

Skip It

  • Simple codebases with direct calls
  • No test suite
  • CI where speed is critical
  • Already getting accurate results

How It Works

1. Call Recording

When you run skylos . --trace, Skylos executes:
import sys

def trace_calls(frame, event, arg):
    if event == 'call':
        filename = frame.f_code.co_filename
        func_name = frame.f_code.co_name
        line = frame.f_code.co_firstlineno
        # Record: (filename, func_name, line)
    return trace_calls

sys.settrace(trace_calls)
pytest.main(["-q"])
sys.settrace(None)

2. Trace File

The results are saved to .skylos_trace:
{
  "version": 1,
  "calls": [
    {
      "file": "/project/visitor.py",
      "function": "visit_FunctionDef",
      "line": 42,
      "count": 156
    },
    {
      "file": "/project/plugin.py",
      "function": "pytest_configure",
      "line": 10,
      "count": 1
    }
  ]
}

3. Cross-Reference

During analysis, Skylos checks each “unused” function against the trace:
Definition: visit_FunctionDef at visitor.py:42
Trace:      visit_FunctionDef at visitor.py:42 (called 156 times)
Result:     ✅ Mark as used

Usage

Basic

skylos . --trace

With Other Flags

skylos . --trace --danger --quality

Reusing Trace Data

The .skylos_trace file persists. On subsequent runs, Skylos uses it automatically:
# First run - generates trace
skylos . --trace

# Later runs - reuses existing trace
skylos .
Commit .skylos_trace to your repo if your test suite is stable. CI runs will use it without re-running tests.

Best Practices

1. Good Test Coverage = Better Trace

The trace only captures functions that run during tests. If a function isn’t tested, it won’t appear in the trace.
# Check your coverage first
pytest --cov=. --cov-report=term-missing

2. Regenerate After Major Changes

If you add new dynamic patterns, regenerate the trace:
rm .skylos_trace
skylos . --trace

3. Exclude Test Files from Trace

By default, Skylos excludes pytest internals. Your test functions themselves are recorded, which is correct - they verify that test code is executed.

4. CI Integration

# .github/workflows/skylos.yml
- name: Run Skylos with Tracing
  run: |
    # Use cached trace if available
    if [ ! -f .skylos_trace ]; then
      skylos . --trace
    else
      skylos .
    fi

Comparison

ApproachAccuracySpeedSetup
Static only70-85%FastNone
Static + Framework rules85-95%FastNone
Static + --trace95-99%Slower (runs tests)Requires tests

Troubleshooting

”No tests ran”

The trace runs pytest by default. If your tests are elsewhere:
# Ensure pytest can find your tests
pytest --collect-only

Trace file not created

Check if pytest is failing:
pytest -q

Still getting false positives

Some patterns can’t be traced:
  • Code only called in production (not in tests)
  • External process calls
  • C extensions
For these, use # pragma: no skylos to suppress:
def production_only_handler():  # pragma: no skylos
    ...

Next Steps