Skip to main content

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:

  1. Import scanning: Detects from flask import Flask, import django, etc.
  2. Decorator recognition: Identifies @app.route, @receiver, @pytest.fixture
  3. Base class analysis: Recognizes class MyView(APIView), class Config(BaseModel)
  4. Pattern matching: Identifies urlpatterns, INSTALLED_APPS, naming conventions
  5. 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

ConfidenceMeaningAction
100%Definitely unusedSafe to delete
80-99%Very likely unusedReview briefly
60-79%Probably unusedReview carefully
< 60%Not flaggedBelow threshold (default)
0%Whitelisted/FrameworkNever 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:

MethodRequired Base Class
save, delete, cleanModel
get, post, put, deleteView, APIView, etc.
get_queryset, get_context_dataView subclasses
list, create, retrieve, update, destroyViewSet
validate, to_representationSerializer
has_permission, has_object_permissionBasePermission
# 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.

PatternPenaltyReason
test_*-40Test discovery (only in test files)
*_test-40Test discovery (only in test files)
clean_*-25Django form validation (only if Django detected)
validate_*-25Django/DRF validation (only if Django detected)
visit_*-25AST visitor dispatch via getattr(self, f"visit_{node_type}")
leave_*-25libcst visitor pattern
handle_*-20Event dispatch via getattr(obj, f"handle_{action}")
*_handler-20Callback/event handlers
*_callback-20Async callbacks
*Plugin-20Plugin discovery via Base.__subclasses__()
on_*-15Event listener pattern
pytest_*-30Pytest 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

ConditionPenaltyReason
Dynamic module (eval/exec in file)-10Functions might be called dynamically
Private name (_helper)-80Less likely to be public API
In __init__.py-15Often 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 --trace to detect

For dynamic patterns, use --trace to cross-reference with runtime data:

skylos . --trace  # Run tests with tracing, then analyze