Skip to main content

Dead Code Detection

The Hidden Cost of Dead Code

Dead code isn't just clutter. It's active technical debt that:

  • Slows Development - Developers read and reason about code that does nothing
  • Hides Bugs - Unused code paths can mask logic errors and security issues
  • Increases Attack Surface - Vulnerable code that's "not used" can still be exploited
  • Bloats Bundles - Unused imports increase load times and memory usage

The problem? Removing code is scary. What if it's actually used somewhere you didn't check?


How Skylos Finds Dead Code

Skylos builds a complete reference graph of your codebase, then identifies definitions with zero references.

What Gets Detected

CategoryExampleDetection Method
Unreachable functionsdef helper(): ... never calledNo call references found
Unused importsimport json but json never usedNo name references found
Unused classesclass OldModel: ... never instantiatedNo instantiation or inheritance
Unused variablesresult = compute() but result never readAssigned but never referenced
Unused parametersdef fn(a, b): return aParameter b never used in body

The Confidence System

Not all "unused" code is actually dead. A helper function might be:

  • Called dynamically via getattr()
  • Used by a framework implicitly
  • Part of a public API

Skylos assigns a confidence score (0-100) to each finding, so you can filter out uncertain results.

Confidence Penalties

PatternPenaltyReason
Private name (_foo)-80Convention for internal use
Dunder (__str__)-100Called implicitly by Python
Underscore var (_)-100Intentionally unused
In __init__.py-15Often public API re-exports
Framework decorator-40Called by framework
Dynamic module-40May use getattr()
Test-related-100Called by test runner

Using Confidence Threshold

# Default: only findings with ≥60% confidence
skylos .

# Include more uncertain findings
skylos . --confidence 40

# Only high-confidence findings
skylos . --confidence 80

Framework Awareness

Skylos understands that framework code is called implicitly:

# Not flagged - Django calls this via URL routing
def user_detail(request, pk):
return render(request, 'user.html', {'user': User.objects.get(pk=pk)})

# Not flagged - Signal receiver
@receiver(post_save, sender=User)
def create_profile(sender, instance, **kwargs):
Profile.objects.create(user=instance)

# Not flagged - Class-based view methods
class UserView(View):
def get(self, request):
return HttpResponse("Hello")

Custom Entrypoints

For proprietary frameworks, plugin loaders, and decorator registries, add project-specific entrypoint rules instead of broad whitelists:

[[tool.skylos.dead_code.entrypoints]]
type = "method"
name = ["create", "pre_hook", "post_hook"]
parent = { name = "Main", base_classes = ["Application"] }
reason = "project framework lifecycle hook"

[[tool.skylos.dead_code.entrypoints]]
type = "function"
decorators = ["runtime_hook"]
path = "src/**"
reason = "registered by runtime_hook"

Skylos treats matching definitions as live while still reporting unrelated dead code in the same files. See Configuration for every supported match field. Entrypoint rules must include a symbol selector such as name, decorators, base_classes, or parent; path, module, and type only narrow the match.


Smart Tracing (Runtime Analysis)

Static analysis can't catch everything. When code is called dynamically via getattr(), visitor patterns, or reflection, Skylos may flag it as unused.

The solution: Run your tests with call tracing enabled.

skylos . --trace

This:

  1. Runs your test suite with sys.settrace() enabled
  2. Records every function that was actually called
  3. Uses that data to eliminate false positives

What Gets Captured

PatternStatic AnalysisWith --trace
visitor.visit(node)visit_FunctionDef()MissedCaught
getattr(obj, "method")()MissedCaught
Plugin hooks (pytest_configure)MissedCaught
Reflection / dynamic importsMissedCaught

When to Use --trace

Use It:

  • Projects with visitor patterns (AST, CST)
  • Plugin architectures
  • Heavy use of getattr() / reflection
  • Many false positives from static analysis

Skip It:

  • Simple codebases with direct calls
  • No test suite available
  • CI where speed matters (tracing adds overhead)

How It Works

tip

Tip: Commit .skylos_trace to your repo if your test suite is stable. Then skylos . will use it automatically without re-running tests.


Comparison: Why Not Just Use Your IDE?

FeatureIDE "Unused" WarningSkylos
Cross-file analysis❌ Single file✅ Entire codebase
Framework awareness✅ Django, Flask, FastAPI, Pydantic
Confidence scoring✅ Filter uncertain findings
CI/CD integration✅ Block PRs, generate reports
Batch removal✅ Interactive selection
Import trackingBasic✅ Resolves re-exports

Safe Removal Workflow

1. Scan with high confidence

Start with findings you can trust:

skylos . --confidence 80

2. Preview deterministic cleanup

For safe unused import/function cleanup, preview the exact edits first:

skylos clean . --dry-run --types import,function --confidence 80

3. Apply selected cleanup

Apply the same deterministic cleanup without prompts:

skylos clean . --apply --types imports --confidence 80

Use comment-out mode when you want a reversible source marker:

skylos clean . --apply --comment-out --types import,function --confidence 80

skylos clean currently edits unused imports and unused functions only. It skips unsupported finding types and uses CST codemods rather than regex edits.

4. Review in interactive mode

Select what to remove:

skylos . -i --dry-run

5. Remove or comment out interactively

# Delete selected items
skylos . -i

# Or comment out (safer)
skylos . -i --comment-out

6. Run tests

Verify nothing broke:

pytest

Comment-Out Mode

Instead of deleting, Skylos can comment out code with a marker:

# Before
def unused_helper():
return "I'm not used"

# After --comment-out
# SKYLOS DEADCODE: def unused_helper():
# SKYLOS DEADCODE: return "I'm not used"

Search for SKYLOS DEADCODE later to permanently remove or restore.


Output Formats

─────────────────── Unreachable Functions ───────────────────
# Name Location Conf
1 unused_helper utils.py:42 90%
2 legacy_processor core/processing.py:128 75%

────────────────────── Unused Imports ───────────────────────
# Name Location
1 json api/views.py:3
2 Optional models.py:1
ColumnMeaning
NameThe unused function, import, class, or variable
Locationfile:line where it's defined
ConfConfidence score (0–100%) — how certain Skylos is that this is truly unused. Higher = safer to remove

Real-World Impact

Case Study: E-commerce Platform

A 200K LOC Python codebase ran Skylos and found:

  • 47 unused functions (3,200 lines of dead code)
  • 156 unused imports (faster startup, smaller bundles)
  • 12 unused classes (legacy models never migrated)

Result: 15% reduction in codebase size, faster CI builds, easier onboarding for new developers.


Next Steps