skip to content

Why must a plain bool @typing.overload stub come after the Literal[True] one?

level: middleimportance: should knowfreq 32%

answer

  1. Candidates are tried in source order
  2. Specific first, catch-all last
  3. A broad stub shadows narrow ones
  4. Literal[True] is a subtype of bool
  5. The fallback returns the union

basics

~20 s

A type checker tries overload stubs top to bottom and takes the first whose parameters accept the call. A plain bool stub accepts every call, so placing it first shadows the Literal[True] and Literal[False] stubs and hands every caller the wide union return.

solid answer

~50 s

Overload resolution is order-sensitive: stubs are candidates in source order and the first one that accepts the arguments wins, so narrow signatures go first and catch-alls go last. `Literal[True]` and `Literal[False]` are subtypes of `bool`, so a `bool` stub placed above them matches every call and the specific stubs become unreachable — the precision you wrote them for disappears. The `bool` stub still belongs at the bottom, because a caller holding a variable of type `bool` rather than a literal matches neither specific stub, and its return has to be the union of theirs, which is the honest answer when the flag is not known statically. The related diagnostic is *overlapping overloads with incompatible return types*: an earlier stub that accepts everything a later one accepts while returning something unrelated, the classic case being an `int` stub above a `bool` stub, since `bool` subclasses `int`.

code

python · 14 lines
python
from typing import Literal, overload

@overload
def to_payload(rows: list[float], *, csv: Literal[True]) -> str: ...
@overload
def to_payload(rows: list[float], *, csv: Literal[False]) -> bytes: ...
@overload
def to_payload(rows: list[float], *, csv: bool) -> str | bytes: ...
def to_payload(rows: list[float], *, csv: bool) -> str | bytes:
    body = ",".join(format(r, ".4f") for r in rows)
    return body if csv else body.encode("utf-8")

print(to_payload([2.4, 0.1], csv=True))
print(to_payload([2.4, 0.1], csv=False))

go deeper

for a junior

Recall the shape of the rule rather than the theory: narrow signatures go at the top, the general one goes at the bottom. If a call is getting a union type back, suspect a broad stub written too early.

for a middle

Explain first-match resolution in source order, why Literal[True] must precede bool, and why the bool fallback still belongs last with a union return. Be able to write the three-stub ordering from memory.

for a senior

Diagnose the overlap warning in real code — bool under int, an object or catch-all parameter high in the list, an optional parameter widening an early stub — and say why reordering, not silencing, is the fix.

for a principal

Set the standard: how many stubs an API may carry before the ordering becomes a maintenance hazard, and whether a flag-shaped parameter deserves overloads at all rather than two clearly named functions.

## Resolution is first-match, in source order A type checker treats the decorated stubs as an ordered list of candidates. For a given call it walks them from the top and selects the first stub whose parameters accept the supplied arguments; that stub's return type becomes the type of the expression. The implementation's own signature is not a candidate — it is checked separately for consistency, but calls never resolve against it. Order is therefore semantic, not cosmetic. Whatever sits above shadows whatever sits below for any call both would accept. ## Why the Literal stubs must be above the bool stub `Literal[True]` and `Literal[False]` describe exactly one value each, and both are subtypes of `bool`: every value they accept, `bool` also accepts. So a `bool` stub is strictly broader, and if it comes first it swallows every call: ```python from typing import Literal, overload @overload def to_payload(rows: list[float], *, csv: bool) -> str | bytes: ... # WRONG: first @overload def to_payload(rows: list[float], *, csv: Literal[True]) -> str: ... # unreachable ``` A call with `csv=True` matches the broad stub, so the call site is typed `str | bytes` and every caller is back to narrowing a union — the exact problem the overloads were written to remove. Reversed, the literal stubs match the literal calls and the broad one only catches what they miss. ## Why the bool fallback stub exists at all It is tempting to delete the broad stub once the two literal ones are in place. Do that, and this stops type-checking: ```python flag = load_flag() # a variable of type bool, not a literal to_payload(rows, csv=flag) # matches neither Literal stub ``` When the flag is only known to be *some* `bool`, no literal stub accepts it, and with no fallback the call is an error. The fallback's job is to accept that case, and its return must be the union of the specific returns — `str | bytes` — because that genuinely is all anyone can know. The ordering rule and the fallback rule are two halves of the same idea: specific first, general last, general returns the union. ## Overlapping stubs with incompatible returns The other ordering hazard is the diagnostic checkers phrase as *overlapping overloads with incompatible return types*. It fires when an earlier stub accepts every call a later stub accepts, but promises an unrelated return type. The subtle instance in Python is numeric: ```python @overload def tag(x: int) -> int: ... @overload def tag(x: bool) -> str: ... # unreachable: bool is a subclass of int ``` `bool` subclasses `int`, so `tag(True)` matches the first stub and is typed `int`, never `str`. The second stub can never be selected, and the declared behaviour for booleans is a lie. Swapping them fixes it: `bool` first, `int` second, and each call gets the type it deserves. The same trap appears with `object` or `Any` parameters near the top of a stub list, with an untyped `*args, **kwargs` catch-all stub written first, and with an optional parameter that makes an earlier stub accept a call an author believed only a later one could take. ## Practical rules - Sort stubs from most specific to most general; never let a catch-all sit above a specific one. - Give the catch-all a return type that is compatible with — usually the union of — everything below it, so the specific stubs remain refinements rather than contradictions. - Remember that `bool` is a subtype of `int`, and `Literal[...]` values are subtypes of their base type; those two facts cause most real overlap warnings. - Do not let ordering carry meaning at runtime. Nothing in this section changes execution: one function object exists, and its body still branches on the flag itself. ## How to say it "Resolution is first-match top-down, so ordering is part of the meaning. `Literal[True]` is narrower than `bool`, so it has to be above it — otherwise the broad stub shadows the specific ones and every caller gets the union back. I still keep the `bool` stub last, returning the union, for callers whose flag is a variable rather than a literal."

  • Why keep a plain bool overload stub once the two Literal stubs exist?
    Because a caller whose flag is a `bool` variable rather than a literal matches neither specific stub, and the call would be rejected. The fallback accepts that case, and its return must be the union of the specific returns — which is honest, since nothing at the call site knows which branch will run.
  • What does an overlapping-overloads-with-incompatible-return-types warning actually mean?
    That an earlier stub accepts every call a later stub accepts while promising an unrelated return, so the later stub is unreachable and the earlier one mistypes part of its range. The classic Python instance is an `int` stub above a `bool` stub, because `bool` subclasses `int`; reordering usually fixes it.
  • Does stub order change anything at runtime?
    No. Order is meaningful only to the checker's resolution algorithm. At runtime the stubs are placeholders that were rebound away, one function object exists, and the body has to branch on the flag value itself — reordering the stubs cannot change which code path executes.

saying these in an interview costs you the question

  • Thinks stub order is cosmetic
  • Puts the catch-all stub first
  • Says the checker picks the most specific stub regardless of order
  • Expects stub ordering to affect runtime dispatch
  • Gives the fallback a return unrelated to the specific stubs
  • Forgets that bool is a subtype of int

context