How can a branch in a Python if/elif chain become unreachable?
answer
- Something above ate the input
- Ask what each branch is really tested under
- Ordering thresholds the wrong way
- isinstance and subclasses
- Branch coverage, not line coverage
basics
~20 sA branch is unreachable when an earlier condition is true for every input that would have matched it. Broad-before-narrow thresholds and a base class tested before its subclass are the usual causes, and Python never warns.
solid answer
~50 sBecause the first truthy condition wins, an earlier branch **shadows** any later branch whose inputs it already accepts. The classic shapes are numeric thresholds ordered from loose to tight -- `if score >= 60: ... elif score >= 90:` makes the second branch dead -- and type tests ordered base-before-subclass, where `isinstance(x, int)` swallows `True` because `bool` subclasses `int`. A repeated or logically implied condition does the same. **CPython reports none of this**: it only folds away literal-constant conditions such as `if False:`, and runtime conditions get no reachability analysis, no error and no warning. You find these with branch coverage rather than line coverage, with a static checker, and with the review rule *narrow before wide*. The structural fix is to order the conditions from most specific to most general, or to make them mutually exclusive by partitioning on one key.
code
python · 17 linesdef band_broken(score):
if score >= 60:
return "pass"
elif score >= 90:
return "distinction"
return "fail"
def band_fixed(score):
if score >= 90:
return "distinction"
elif score >= 60:
return "pass"
return "fail"
print(band_broken(95), band_fixed(95)) # pass distinctiongo deeper
Know that only the first true condition wins, so a broad test placed early hides the specific test below it. Ordering numeric checks from the highest threshold down is the habit to build.
Explain the mechanics: each branch is implicitly tested under "and nothing above matched", bool subclasses int so isinstance chains must go most-derived first, and CPython issues no diagnostic for a dead runtime branch.
Demonstrate detection and prevention on real code: branch coverage over line coverage, a test per branch that asserts the outcome, and restructuring so the conditions are mutually exclusive rather than merely ordered correctly today.
Own the standard. Decide where overlapping-plus-ordered conditions are a legitimate precedence design versus an accident waiting to be reordered, and make that intent explicit in the code so the next editor cannot break it silently.
### What "unreachable" means in a chain A branch is unreachable when there is no input for which it is the **first** truthy condition. The branch is syntactically fine, it compiles, it may even be covered by a reviewer's mental model -- it just never executes, because some earlier condition accepts everything it would have accepted. This follows directly from first-match-wins. The chain is ordered, so each branch is really tested under an implicit *and none of the ones above matched*. Once you read a chain that way, the dead branches become visible. ### Shape 1: thresholds ordered loose before tight ```python def band(score): if score >= 60: return "pass" elif score >= 90: # dead: 90 already satisfies 60 return "distinction" return "fail" ``` Every score that satisfies `>= 90` also satisfies `>= 60`, so the second branch can never win. The fix is ordering, not extra conditions: test `>= 90` first. People reach instead for `elif 60 <= score < 90:` and add a redundant bound that will drift out of sync with the neighbouring branch the next time the grading changes. Prefer descending thresholds with the boundaries named once. ### Shape 2: a base class tested before its subclass ```python def describe(value): if isinstance(value, int): return "integer" elif isinstance(value, bool): # dead: bool is a subclass of int return "boolean" return "other" describe(True) # 'integer' ``` `isinstance` is true for subclasses, and in Python `bool` genuinely subclasses `int` -- `isinstance(True, int)` is `True`. The same trap appears with any user-defined hierarchy and with exception handling, where `except Exception` before a narrower handler makes the narrower one dead. Order type tests **most derived first**. ### Shape 3: implication and duplication A condition that is logically implied by an earlier one is dead even when the two look unrelated: `if not items:` followed by `elif items is None:` can never reach the second branch, because `None` is falsy and was already caught. Copy-paste duplication (`elif status == "paid"` twice) is the crude version of the same bug, and it is the one a reviewer spots most easily. ### What CPython does and does not tell you The compiler evaluates literal constant conditions at compile time and drops the block entirely -- an `if False:` body does not survive into the bytecode, which you can see with `dis.dis` or `dis.get_instructions`. That is constant folding, not reachability analysis. For any condition that depends on a runtime value, CPython performs **no** analysis at all: no error, no `SyntaxWarning`, no runtime complaint. A dead branch will sit in production for years. ### How you actually catch them - **Branch coverage, not line coverage.** A line-coverage tool can report a chain as fully covered while a branch has never been the winning one, because the line was executed as part of the condition test. Branch (or arc) coverage distinguishes taken from not-taken and is the tool that surfaces this class of defect. - **A test per branch, asserting the outcome.** Write one case that is meant to land in each branch and assert the value it produces. A dead branch fails that test immediately -- and unlike coverage, the test also documents the intended boundaries. - **Static analysis.** Type checkers narrow types along a chain and can report a branch as unreachable once the preceding conditions have exhausted the type; linters flag duplicated conditions. Neither catches arbitrary arithmetic implications, so they are a floor, not a guarantee. - **The review rule: narrow before wide.** Read the chain top to bottom and, for each branch, ask what an earlier condition already ate. It takes seconds and it is the only check that works on conditions no tool can reason about. ### Fix the structure, not just the order Reordering makes the chain correct today. Making the conditions **mutually exclusive by construction** makes it stay correct: partition on a single value so no two branches can both be true, extract the boundaries into named constants used exactly once, or move the boundaries into data. Then a future edit that inserts a branch in the wrong place is a visible mistake rather than a silent one. Be careful in the opposite direction too: if a chain's conditions overlap on purpose -- a deliberate precedence ordering -- then reordering it to "tidy it up" is a behaviour change. Overlap plus order is a legitimate design; overlap plus *accidental* order is the bug.
- Why is an `isinstance(x, bool)` branch dead when it follows an `isinstance(x, int)` branch?Because `bool` is a subclass of `int` in Python, so `isinstance(True, int)` is `True` and the earlier branch always wins for booleans. Type tests in a chain must run most-derived first. The same rule governs `except` clauses, where a broad handler placed first makes every narrower handler below it unreachable.
- Does CPython warn about an unreachable elif branch?No. The compiler folds literal-constant conditions -- an `if False:` block does not appear in the bytecode at all, as `dis.dis` shows -- but it performs no reachability analysis on conditions involving runtime values. There is no error and no warning; you need branch coverage, a per-branch test, or a static checker.
- Why is line coverage a poor tool for finding a dead branch?Because the condition line is executed whenever the chain is evaluated, so it reports as covered even though the branch was never the winning one. Branch (arc) coverage records taken versus not-taken separately and exposes the gap. A test that asserts the outcome for each intended input is stronger still, since it also pins the boundaries.
saying these in an interview costs you the question
- Expects Python to raise an error for an unreachable branch
- Orders numeric threshold checks from smallest to largest
- Thinks isinstance(x, int) excludes True and False
- Believes moving an elif branch never changes behaviour
- Relies on line coverage to prove every branch was taken
- Adds redundant upper bounds instead of reordering the chain