Framework Awareness
Skylos recognizes patterns from popular Python frameworks to reduce false positives in dead code detection. Framework-specific functions that are called implicitly by the framework are not flagged as unused.
Supported Frameworks
- Django (views, signals, URL routing, DRF)
- Flask (routes, error handlers)
- FastAPI (routes, dependencies)
- Pydantic (models, validators)
- Celery (tasks)
- Pytest (fixtures, hooks)
- AST visitors (libcst, lark, etc.)
How Detection Works
Skylos identifies framework usage through:
- Import scanning: Detects
from flask import Flask,import django, etc. - Decorator recognition: Identifies
@app.route,@receiver,@pytest.fixture - Base class analysis: Recognizes
class MyView(APIView),class Config(BaseModel) - Pattern matching: Identifies
urlpatterns,INSTALLED_APPS, naming conventions - Known patterns: Applies confidence penalties to names like
handle_*,visit_*
Confidence System
Skylos assigns a confidence score (0-100%) to each finding. Higher confidence = more certain it's dead code.
How Confidence is Calculated
Starting confidence: 100%
- Soft pattern penalties (e.g., handle_* = -20)
- Dynamic module penalty (file has eval/exec = -10)
- Framework detection (route decorator = 0%, skip entirely)
- Private name penalty (_helper = -80)
= Final confidence
Confidence Thresholds
| Confidence | Meaning | Action |
|---|---|---|
| 100% | Definitely unused | Safe to delete |
| 80-99% | Very likely unused | Review briefly |
| 60-79% | Probably unused | Review carefully |
| < 60% | Not flagged | Below threshold (default) |
| 0% | Whitelisted/Framework | Never flagged |
skylos . -c 60 # Default: only flag >= 60% confidence
skylos . -c 40 # Include more uncertain findings
skylos . -c 0 # Show everything (debug mode)
Hard Entrypoints (Confidence = 0%)
These are never flagged - Python or frameworks call them automatically:
Magic Methods
__init__, __new__, __del__, __repr__, __str__
__enter__, __exit__, __iter__, __next__
__getattr__, __setattr__, __getitem__, __setitem__
__call__, __len__, __hash__, __eq__, __lt__
__post_init__ # dataclasses
# ... and all other dunder methods
Pytest Hooks
pytest_configure, pytest_unconfigure, pytest_addoption
pytest_collection_modifyitems, pytest_runtest_setup
pytest_fixture_setup, pytest_generate_tests
Unittest Lifecycle
setUp, tearDown, setUpClass, tearDownClass
setUpModule, tearDownModule
Django Methods (with base class check)
These are only skipped when the class inherits from the correct base:
| Method | Required Base Class |
|---|---|
save, delete, clean | Model |
get, post, put, delete | View, APIView, etc. |
get_queryset, get_context_data | View subclasses |
list, create, retrieve, update, destroy | ViewSet |
validate, to_representation | Serializer |
has_permission, has_object_permission | BasePermission |
# This is skipped (correct base class)
class UserView(APIView):
def get(self, request): # confidence = 0%
pass
# This is NOT skipped (no framework base)
class Helper:
def get(self, request): # confidence = 100% (flagged)
pass
Soft Patterns (Confidence Penalties)
These patterns reduce confidence but don't skip entirely. The function is still flagged if confidence remains above threshold.
| Pattern | Penalty | Reason |
|---|---|---|
test_* | -40 | Test discovery (only in test files) |
*_test | -40 | Test discovery (only in test files) |
clean_* | -25 | Django form validation (only if Django detected) |
validate_* | -25 | Django/DRF validation (only if Django detected) |
visit_* | -25 | AST visitor dispatch via getattr(self, f"visit_{node_type}") |
leave_* | -25 | libcst visitor pattern |
handle_* | -20 | Event dispatch via getattr(obj, f"handle_{action}") |
*_handler | -20 | Callback/event handlers |
*_callback | -20 | Async callbacks |
*Plugin | -20 | Plugin discovery via Base.__subclasses__() |
on_* | -15 | Event listener pattern |
pytest_* | -30 | Pytest hook functions |
Example Calculation
# File has eval() somewhere, so dynamic_module penalty applies
def handle_secret(): # 100 - 20 (handle_*) - 10 (dynamic) = 70%
pass
def visit_Name(): # 100 - 25 (visit_*) - 10 (dynamic) = 65%
pass
def totally_dead(): # 100 - 10 (dynamic) = 90%
pass
Context-Aware Penalties
Some penalties are reduced when context doesn't match:
# In a test file (tests/test_user.py):
def test_login(): # 100 - 40 = 60% (full penalty)
pass
# In a non-test file (app.py):
def test_login(): # 100 - 10 = 90% (penalty / 4)
pass
Other Penalties
| Condition | Penalty | Reason |
|---|---|---|
Dynamic module (eval/exec in file) | -10 | Functions might be called dynamically |
Private name (_helper) | -80 | Less likely to be public API |
In __init__.py | -15 | Often re-exports |
Django
Views
Both function-based and class-based views are recognized:
# Function-based view - not flagged if in urlpatterns
def my_view(request):
return HttpResponse("Hello")
# Class-based view - methods not flagged (base class detected)
class MyView(View):
def get(self, request): # confidence = 0%
return HttpResponse("Hello")
def post(self, request): # confidence = 0%
return HttpResponse("Created")
URL Patterns
Functions referenced in urlpatterns are marked as used:
urlpatterns = [
path('home/', home_view), # home_view marked as used
path('api/', include('api.urls')),
]
Signals
Signal receivers are recognized:
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=User) # confidence = 0%
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
Django REST Framework
ViewSet methods and serializer fields are recognized:
class UserViewSet(viewsets.ModelViewSet):
# These methods are not flagged (base class detected)
def list(self, request): # confidence = 0%
pass
def create(self, request): # confidence = 0%
pass
def retrieve(self, request, pk=None): # confidence = 0%
pass
Flask
Routes
Route handlers are recognized via decorators:
@app.route('/hello') # confidence = 0%
def hello():
return 'Hello, World!'
@app.get('/users') # confidence = 0%
def get_users():
return jsonify(users)
Error Handlers and Middleware
@app.errorhandler(404) # confidence = 0%
def not_found(error):
return 'Not Found', 404
@app.before_request # confidence = 0%
def before():
pass
@app.after_request # confidence = 0%
def after(response):
return response
FastAPI
Route Handlers
@router.get('/items') # confidence = 0%
async def get_items():
return []
@app.post('/items') # confidence = 0%
async def create_item(item: Item):
return item
Dependency Injection
Functions used as dependencies are recognized:
async def get_db(): # Recognized when used in Depends()
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get('/users')
async def get_users(db: Session = Depends(get_db)): # get_db not flagged
return db.query(User).all()
Request Models
Pydantic models used as type hints in routes are recognized:
class CreateUser(BaseModel): # confidence = 0% (used in route below)
name: str
email: str
@app.post('/users')
async def create_user(user: CreateUser): # CreateUser recognized
return user
Pydantic
Models
Classes inheriting from BaseModel have special handling:
class UserConfig(BaseModel): # Not flagged if referenced
name: str
age: int
class Config: # Inner Config class not flagged
extra = 'forbid'
Validators
class User(BaseModel):
email: str
@field_validator('email') # confidence = 0%
def validate_email(cls, v):
if '@' not in v:
raise ValueError('Invalid email')
return v
@model_validator(mode='after') # confidence = 0%
def validate_model(self):
return self
AST Visitors
Skylos recognizes the visitor pattern used by ast, libcst, lark, etc.:
class MyVisitor(ast.NodeVisitor):
def visit_FunctionDef(self, node): # confidence = 75% (100 - 25)
pass
def visit_Name(self, node): # confidence = 75% (100 - 25)
pass
def leave_FunctionDef(self, node): # confidence = 75% (100 - 25)
pass
These are called via getattr(self, f"visit_{node.__class__.__name__}"), so they appear unused in static analysis.
Testing Frameworks
Pytest
@pytest.fixture # confidence = 0%
def db_session():
session = create_session()
yield session
session.close()
def test_user_creation(db_session): # confidence = 0% (test_ prefix in test file)
user = User(name='Test')
db_session.add(user)
Unittest
class TestUser(unittest.TestCase):
def setUp(self): # confidence = 0%
self.user = User()
def tearDown(self): # confidence = 0%
pass
def test_name(self): # confidence = 0%
self.assertEqual(self.user.name, 'default')
Decorator Patterns
Skylos recognizes these decorator patterns (confidence = 0%):
# Route decorators
@*.route
@*.get, @*.post, @*.put, @*.delete, @*.patch
@*.head, @*.options, @*.trace
# Lifecycle decorators
@*.before_request, @*.after_request
@*.teardown_*, @*.on_event
# Middleware
@*.middleware, @*.exception_handler
# Auth decorators
@*_required, @login_required, @permission_required
# Validators (Pydantic)
@validator, @field_validator, @model_validator
@root_validator, @field_serializer, @model_serializer
# Testing
@pytest.fixture, @pytest.mark.*
Configuration Class Handling
Classes named Settings, Config, or ending with those suffixes have their attributes excluded:
class AppSettings: # Attributes not flagged
DEBUG = True
DATABASE_URL = "postgresql://..."
class Config: # Attributes not flagged
SECRET_KEY = "..."
Suppressing False Positives
If Skylos flags something incorrectly, you have several options:
1. Inline Ignore (one-off)
def dynamic_handler(): # skylos: ignore
pass
2. Whitelist Pattern (permanent)
skylos whitelist 'handle_*'
skylos whitelist my_func --reason "Called via registry"
3. Config File
[tool.skylos.whitelist]
names = ["handle_*", "*Plugin"]
[tool.skylos.whitelist.documented]
"my_func" = "Called dynamically via registry lookup"
4. Lower Confidence Threshold
skylos . -c 40 # Include more uncertain findings
Limitations
Framework awareness is based on static analysis and cannot detect all usage patterns:
- Dynamic routing: Routes defined at runtime may not be detected
- Plugin systems: Code loaded via plugins may appear unused
- Meta-programming: Heavily meta-programmed code may produce false positives
- String dispatch:
globals()[func_name]()patterns need--traceto detect
For dynamic patterns, use --trace to cross-reference with runtime data:
skylos . --trace # Run tests with tracing, then analyze