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:
- Runs
pytestwith call tracing enabled - Records every function call to
.skylos_trace - Runs static analysis
- Cross-references findings with trace data
- Removes false positives
What Gets Captured
| Pattern | Static Analysis | With --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
| Approach | Accuracy | Speed | Setup |
|---|---|---|---|
| Static only | 70-85% | Fast | None |
| Static + Framework rules | 85-95% | Fast | None |
Static + --trace | 95-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
- Dead Code Detection - Learn how Skylos finds unused code
- Framework Awareness - Built-in patterns for Django, Flask, FastAPI