Skip to main content

Smart Tracing

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_FunctionDefMissedCaught
getattr(obj, "method")()MissedCaught
globals()["func_name"]()MissedCaught
Plugin hooks (pytest_configure)MissedCaught
Signal handlers (@receiver)MissedCaught
Dynamic imports + callsMissedCaught

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 .
tip

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