Skip to main content

Inheritance & Type Pattern Detection

Static analyzers struggle with Python's dynamic typing patterns. Code can be used in ways that aren't visible through simple reference tracking - inheritance, duck typing, framework magic, and data class machinery.

Skylos will handle most of these patterns.


Patterns We Detect

CategoryPatterns
ProtocolsProtocol classes, Protocol members, Protocol implementers, duck-typed implementations
ABCsABC classes, @abstractmethod methods, ABC implementers
Mixins*Mixin class methods
Base ClassesBase*, *Base, *ABC, *Interface, *Adapter
Test DoublesMock*, Fake*, Stub*, *Mock, *Fake, etc.
Data Classes@dataclass, NamedTuple, TypedDict, attrs, Pydantic
Enumsenum.Enum members
ORM ModelsSQLAlchemy columns, Django model fields
Type SystemType aliases, TypeVar, ParamSpec
Config Classes*Settings, *Config class attributes

Protocol & ABC Detection

The Problem

from typing import Protocol

class DataStore(Protocol):
def get(self, key: str) -> str: ...
def set(self, key: str, value: str) -> None: ...

class RedisStore: # No explicit inheritance
def get(self, key: str) -> str:
return self.client.get(key)

def set(self, key: str, value: str) -> None:
self.client.set(key, value)

A naive analyzer will flag all of these methods as unused. But they're clearly designed to work together.

How Skylos Handles It

  1. Protocol classes -> Detected and skipped entirely
  2. ABC classes -> Abstract methods tracked for matching
  3. Explicit implementers -> Classes inheriting from Protocol/ABC skipped
  4. Duck-typed implementers -> Classes matching ≥70% of Protocol methods (min 3) detected

Cross-File Detection

All detection works globally across files:

# protocols.py
class DataStore(Protocol):
def get(self, key): ...

# redis_store.py
class RedisStore: # Different file, still detected!
def get(self, key): ...

Mixin & Base Class Detection

Mixin Classes

Methods in *Mixin classes are called via inheritance, not direct reference:

class LoggingMixin:
def log_action(self, action): # Called by child classes
print(f"Action: {action}")

class UserService(LoggingMixin):
def create_user(self, name):
self.log_action("create_user") # Calls mixin method

Result: LoggingMixin.log_action gets a -60% confidence penalty (it will not be skipped, but lower priority).

Abstract Base Classes

Methods in classes with "base" naming are meant to be overridden:

class BaseHandler:  # Prefix pattern
def handle(self, data): ...

class AbstractValidator: # Contains "Abstract"
def validate(self, value): ...

class StorageInterface: # Suffix pattern
def save(self, obj): ...

Detected patterns: Base*, *Base, *ABC, *Interface, *Adapter

Result: All methods → confidence = 0 (fully skipped)


Test Double Detection

Test doubles implement interfaces for testing purposes:

class InMemoryCache:  # Prefix: InMemory*
def get(self, key): ...
def set(self, key, value): ...

class MockPaymentGateway: # Prefix: Mock*
def charge(self, amount): ...

class DatabaseFake: # Suffix: *Fake
def query(self, sql): ...

Detected prefixes: InMemory*, Mock*, Fake*, Stub*, Dummy*, Fixed*

Detected suffixes: *Mock, *Stub, *Fake, *Double

Result: All methods get -40% confidence penalty


Data Class Field Detection

Data class fields are accessed via the class machinery, not direct reference:

@dataclass

from dataclasses import dataclass

@dataclass
class User:
name: str # Accessed via User(name="x").name
email: str # Looks unused but it is used
age: int = 0

Result: All fields -> confidence = 0

NamedTuple

from typing import NamedTuple

class Point(NamedTuple):
x: float # Accessed via point.x
y: float

Result: All fields -> confidence = 0

TypedDict

from typing import TypedDict

class UserDict(TypedDict):
name: str # Accessed via d["name"]
email: str

Result: All fields -> confidence = 0

attrs

import attr

@attr.s
class Config:
host: str = attr.ib()
port: int = attr.ib(default=8080)

Result: All fields → confidence = 0

Pydantic

from pydantic import BaseModel

class UserModel(BaseModel):
name: str
email: str

Result: All fields -> confidence = 0


Enum Member Detection

Enum members are accessed as class attributes:

from enum import Enum

class Status(Enum):
PENDING = "pending" # Status.PENDING
APPROVED = "approved" # Status.APPROVED
REJECTED = "rejected" # Status.REJECTED

Result: All enum members -> confidence = 0


ORM Model Detection

ORM columns are accessed by the ORM, not direct code:

SQLAlchemy

from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import declarative_base

Base = declarative_base()

class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String) # Used by ORM

Django

from django.db import models

class User(models.Model):
name = models.CharField(max_length=100) # Used by ORM
email = models.EmailField()

Result: All model columns -> confidence = 0


Type Alias Detection

Type aliases define types, not values:

from typing import TypeAlias, List, Dict

type UserId = int
type UserMap = Dict[str, User]

Result: All type aliases -> confidence = 0


Settings/Config Class Detection

Configuration classes have attributes accessed externally:

class AppSettings:
DEBUG = True
API_KEY = "secret"
MAX_RETRIES = 3

class DatabaseConfig:
HOST = "localhost"
PORT = 5432

Detected patterns: *Settings, *Config

Result: All attributes -> confidence = 0

SCREAMING_CASE Class Variables

class MyClass:
MAX_RETRIES = 3 # prob a constant
DEFAULT_TIMEOUT = 30

Result: SCREAMING_CASE class variables get -40% confidence


Framework Lifecycle Methods

Framework methods are called by the framework, not your code:

Event Handlers

class MyWidget:
def on_mount(self):
pass

def on_click(self, event): # Called by framework
pass

Result: on_* methods get -30% confidence

Reactive Watchers

class MyWidget:
def watch_value(self, new_value): # Called reactively
self.update_display()

Result: watch_* methods get -30% confidence

Compose Methods

class MyScreen:
def compose(self): # Called by Textual/React-like frameworks
yield Header()
yield Footer()

Result: compose method gets -40% confidence


BINDINGS Detection

Textual's BINDINGS system maps keys to action_* methods:

class MyApp(App):
BINDINGS = [
("q", "quit", "Quit"),
("c", "copy", "Copy"),
]

def action_quit(self): # Called via BINDINGS
self.exit()

def action_copy(self): # Called via BINDINGS
self.copy_to_clipboard()

How it works:

  1. Skylos parses BINDINGS assignments
  2. Extracts action names ("quit", "copy")
  3. Marks action_* methods as used globally across all classes

Multiple Candidate Resolution

When a reference has multiple matching definitions:

class ResultsMixin:
def action_copy(self): ...

class DataMixin:
def action_copy(self): ... # Same name, different class

# BINDINGS references "action_copy" - which one?

Solution: Mark ALL candidates as potentially used.


What Gets Skipped vs Penalized

Hard Skips (Confidence = 0)

PatternReason
Protocol classes + membersInterface definition
ABC abstract method implementationsRequired by contract
Base*, *Base, *ABC, *Interface, *Adapter methodsAbstract base
Dataclass fieldsData class machinery
NamedTuple fieldsTuple access
Enum membersClass attribute access
Pydantic/attrs/ORM fieldsFramework machinery
Type aliasesType system
Settings/Config attributesExternal configuration

Penalties (Reduced Confidence)

PatternPenaltyReason
*Mixin methods-60%Called via inheritance
Test doubles (Mock*, *Fake, etc.)-40%Test implementation
on_* methods-30%Event handler convention
watch_* methods-30%Reactive watcher convention
compose method-40%Framework lifecycle
SCREAMING_CASE class vars-40%Likely constants
Method name matches abstract-40%Might implement interface

Configuration

All detection is automatic - no configuration required.

To whitelist additional patterns:

[tool.skylos.whitelist]
names = ["*Impl", "*Implementation", "register_*"]

Limitations

  1. Runtime inheritance: class X(get_base()) cannot be detected
  2. Dynamic Protocols: Protocols must be statically defined
  3. 70% threshold: Fixed for duck-typing (may be configurable in future)
  4. Cross-file only in same analysis run: Multi-project analysis not supported

Next Steps

What is Static Analysis?

Learn why unused code is dangerous

What is Taint Analysis?

Deep dive into security analysis