You open a Python file, navigate to a method, and start editing. The syntax is valid. The linter is quiet. But when you run your tests, your changes have no effect. The method you're editing is dead code, shadowed by another class in the inheritance chain.
The setup: a training pipeline
Consider a deep learning codebase with three classes in an inheritance chain:
# base.py
class DLEstimatorBase:
def setup_dataloader(self):
return "base dataloader"
def configure_optimizers(self):
return "SGD"
def train(self):
return "training..."# lightning.py
from base import DLEstimatorBase
class LightningTrainer(DLEstimatorBase):
def configure_optimizers(self):
return "Adam via Lightning"# deepspeed.py
from lightning import LightningTrainer
class DeepSpeedEstimator(LightningTrainer):
def setup_dataloader(self):
return "deepspeed dataloader v2"If you're editing DLEstimatorBase.configure_optimizers, your changes will never execute when LightningTrainer (or any of its subclasses) is instantiated. LightningTrainer's version shadows the base. This is not a bug. It's how Python's method resolution order works.
How Python resolves methods: C3 linearization
When you call estimator.configure_optimizers(), Python doesn't just look at the instance's class. It walks the entire Method Resolution Order (MRO), a deterministic sequence of classes computed at class creation time, and returns the first match.
Python uses the C3 linearization algorithm, introduced in Python 2.3 to replace the older depth-first left-to-right search. C3 guarantees three properties:
- •Monotonicity: If class A comes before class B in the MRO of class C, then A comes before B in the MRO of any subclass of C.
- •Local precedence order: The order in which base classes are listed in the class definition is preserved.
- •Extended precedence graph consistency: If the constraints conflict, Python raises a TypeError at class creation time rather than silently producing a wrong order.
The C3 algorithm, step by step
C3 linearization computes the MRO recursively. For a class C with bases B1, B2, ...:
MRO(C) = C + merge(MRO(B1), MRO(B2), ..., [B1, B2, ...])
merge works as follows:
1. Take the head (first element) of the first non-empty list.
2. If that head does not appear in the tail of ANY other list,
select it: add it to the result and remove it from all lists.
3. If it does appear in a tail, skip to the next list and try its head.
4. Repeat until all lists are empty.
5. If no valid head can be found, the hierarchy is inconsistent.For our DeepSpeedEstimator example:
MRO(DeepSpeedEstimator)
= DeepSpeedEstimator + merge(
MRO(LightningTrainer),
[LightningTrainer]
)
= DeepSpeedEstimator + merge(
[LightningTrainer, DLEstimatorBase],
[LightningTrainer]
)
Step 1: Head of first list = LightningTrainer
Not in tail of any list? Tails: [], []
Yes -> select LightningTrainer
Step 2: Head of first list = DLEstimatorBase
Not in tail of any list? Tails are empty
Yes -> select DLEstimatorBase
Result: [DeepSpeedEstimator, LightningTrainer, DLEstimatorBase]You can verify this in a Python REPL: DeepSpeedEstimator.__mro__ returns the same order. PRISM computes it from source using its own C3 implementation, without importing your code.
The diamond problem
C3 linearization matters most with multiple inheritance. Consider:
class A:
def method(self): return "A"
class B(A):
def method(self): return "B"
class C(A):
def method(self): return "C"
class D(B, C):
pass
# D.__mro__ = [D, B, C, A]
# D().method() returns "B"Without C3, a naive depth-first search would visit D -> B -> A -> C -> A, hitting A before C. That breaks the intuition that C, being a direct base, should be checked before A (a grandparent). C3 correctly produces [D, B, C, A], ensuring every class appears exactly once and local precedence is preserved.
In this diamond, if you're editing A.method, it's shadowed by both B and C. If you're editing C.method, it's shadowed by B (because D lists B first). These relationships are hard to see in large codebases, which is exactly the kind of thing a tool should surface.
The four states of a method
PRISM classifies every method into one of four states based on the MRO analysis:
MRO chain: [DeepSpeedEstimator, LightningTrainer, DLEstimatorBase]
DeepSpeedEstimator LightningTrainer DLEstimatorBase
+--------------------+ +---------------------+ +---------------------+
| setup_dataloader | | configure_optimizers| | setup_dataloader |
| STATUS: overrides | | STATUS: overrides | | STATUS: shadowed |
+--------------------+ +---------------------+ +---------------------+
| configure_optimizers|
| STATUS: shadowed |
+---------------------+
| train |
| STATUS: owns |
+---------------------+- •Owns (green): DLEstimatorBase.train. Only one class defines it. It always runs.
- •Overrides (amber): DeepSpeedEstimator.setup_dataloader. Wins over the base class version.
- •Overridden (purple): A method that wins in its own MRO but is redefined by a descendant. Dead code for subclass instances.
- •Shadowed (red): DLEstimatorBase.configure_optimizers. LightningTrainer's version wins. Dead code.
Why existing tools miss this
Python linters (pylint, flake8, mypy) check for syntax errors, type mismatches, and style violations. They don't compute the MRO at edit time. IDE features like "Go to Definition" and "Find All References" help you navigate, but they don't tell you whether the definition you're looking at is the one that actually executes.
The information exists. Python computes the MRO at class creation. But it's only available at runtime. PRISM extracts it statically by parsing the AST, resolving the class hierarchy across files, and running C3 linearization from source. No imports, no execution, no side effects.
Detection at the speed of thought
PRISM's analysis runs on every cursor movement. The full pipeline (AST parsing, hierarchy resolution, C3 linearization, status classification) completes in under 200 milliseconds. Fast enough that the panel updates feel instant, turning method resolution from an invisible runtime concept into something you can see and reason about as you code.
The next time you're deep in an inheritance chain and wondering whether your edit will actually take effect, look at PRISM's signal bar. If it's red, you're editing dead code.
Try PRISM
See method resolution in real time.