Your ETL export helper's @typing.overload stubs forbid a precision value, yet the nightly job hits it — why?
answer
- The checker never runs in production
- Dynamic data was never checked
- A splatted mapping hides the arguments
- The else branch swallowed the typo
- Validate at the parsing boundary and raise
basics
~20 sOverloads are checked, never enforced. Nothing at runtime consults them, so a value arriving as a plain str from a job config, or through a call the checker never saw, reaches the implementation unchallenged. Only an explicit check inside the implementation can reject it.
solid answer
~50 sThe stubs are erased as far as execution is concerned: one function object exists, and its body runs whatever it is handed. A scheduler that reads `precision` out of a job file has a `str` — or an `Any` from a parsed dict — and splatting that into the call with `**config` hides the argument from the checker entirely, so no stub was ever consulted. Inside, a body written as `if precision == "exact": ... else: ...` treats the unknown value as the fast float path, and a 2.4 GB export silently ships rounding-drifted numbers instead of failing. The fixes are all runtime ones: validate where dynamic data enters and turn the string into a checked value, make the implementation's branching exhaustive with an explicit `raise` instead of a catch-all `else`, and keep the checker running in CI over the calling code so the paths that *are* typed stay honest.
code
python · 18 linesfrom typing import Literal, overload
@overload
def export(rows: list[float], *, precision: Literal["exact"]) -> str: ...
@overload
def export(rows: list[float], *, precision: Literal["fast"]) -> str: ...
def export(rows: list[float], *, precision: str) -> str:
if precision not in {"exact", "fast"}:
raise ValueError(f"unknown precision: {precision!r}")
digits = 10 if precision == "exact" else 2
return ",".join(format(r, f".{digits}f") for r in rows)
job_config = {"precision": "fastest"}
print(export([2.4], precision="exact"))
try:
export([2.4], **job_config)
except ValueError as exc:
print(exc)go deeper
Take away the core fact: annotations and overload stubs never run. If a value must be rejected while the program is executing, some code has to compare it and raise.
Explain the erasure mechanically and name the doors dynamic data comes through — parsed config typed as Any, a ** splat that hides keyword arguments, callers the checker never inspected.
Diagnose the incident end to end: why the run succeeded instead of failing, how a catch-all else turned a typo into a wrong result, and where you would put validation so the failure is loud and early rather than a warehouse mismatch days later.
Own the policy question: which boundaries in the pipeline are allowed to accept untyped input, whether the checker runs over callers as well as libraries, and how much a team should invest in static guarantees that stop at the edge of checked code.
## The one-sentence cause `typing.overload` produces no runtime behaviour. The decorator registers a stub for tooling and returns a placeholder; the final `def` rebinds the name; execution sees a single ordinary function. There is no argument inspection, no dispatch table and no validation. A stub set that forbids `precision="fastest"` forbids it *in code a type checker examined* — nowhere else. ## How the value got in anyway In a nightly export pipeline there are usually three doors, and the incident is normally one of them: 1. **Dynamic data.** The flag came from a job file, a database row or an environment variable. Parsed configuration is typically `dict[str, Any]` or `dict[str, str]`, and `Any` is assignable to everything — the checker raises nothing because it has been told nothing. 2. **A call the checker cannot see.** `export(rows, **job_config)` hides the keyword arguments behind a mapping. Even fully typed, a `dict[str, str]` splat cannot be matched against `Literal` stubs, and many configurations end up with the call unchecked rather than rejected. 3. **Code outside the checked set.** An untyped module, a notebook, a plugin, a `# type: ignore`, or a caller in a repository where the checker is not run in CI. The promise only binds code the checker read. ```python from typing import Literal, overload @overload def export(rows: list[float], *, precision: Literal["exact"]) -> str: ... @overload def export(rows: list[float], *, precision: Literal["fast"]) -> str: ... def export(rows: list[float], *, precision: str) -> str: digits = 10 if precision == "exact" else 2 return ",".join(format(r, f".{digits}f") for r in rows) job_config = {"precision": "fastest"} # a typo in a scheduler file export([2.4], **job_config) # runs; two decimal places ``` Nothing raises. The `else` branch means "fast", so a typo means "fast", and every float in a 2.4 GB export lands with two decimals of precision. Because the job still succeeds, the drift surfaces days later as a reconciliation mismatch against the warehouse rather than as a failed run. ## Why the implementation's shape made it worse The body's `if/else` is the real defect. An `else` that stands for a specific mode silently absorbs every value the author never considered. Written exhaustively, the same body turns a config typo into an immediate, loud failure: ```python def export(rows: list[float], *, precision: str) -> str: if precision == "exact": digits = 10 elif precision == "fast": digits = 2 else: raise ValueError(f"unknown precision: {precision!r}") ... ``` That single `raise` is what the overloads could never provide. It also fails at the top of the run, before 2.4 GB of work, instead of at the end. ## Where validation belongs Push the check to the boundary where untyped data becomes typed data. The parsing layer that loads the job file should convert `precision` from an arbitrary string into a value the rest of the program can trust — membership in an explicit set of allowed strings, an enum member, or a small parse function that raises on anything else. Past that boundary, everything is checked code and the overloads earn their keep again: the stubs give call sites exact return types and catch the mistakes that *are* visible statically, in CI, before deploy. Two supporting habits matter as much: - **Prefer explicit keyword arguments to `**config` splats** at the boundary, so the checker sees each argument. Splatting a dynamic mapping into a precisely typed API discards the precision you paid for. - **Run the checker over the callers, not just the library.** An overload set only constrains code that is actually checked; a library with beautiful stubs and unchecked callers has documentation, not guarantees. ## And when you genuinely need behaviour to vary If the requirement is that different argument *types* select different code, that is runtime dispatch, and it needs a runtime mechanism — a mapping from a validated key to a handler, or a generic function that dispatches on the first argument's type. Overloads stay on top of it as the typing facade; they never become the dispatcher. ## How to answer in the room "Because overloads are erased. They constrain checked call sites and nothing else, and the value came from a config file as a string, so no stub was ever consulted. The real fix is two lines: validate at the boundary where the config is parsed, and make the implementation's branching exhaustive so an unknown mode raises instead of falling into the fast path."
- How do you stop the implementation from silently taking the wrong branch?Make its branching exhaustive: compare against each known value explicitly and `raise` on anything else, instead of letting a bare `else` stand for one of the real modes. Fail at the start of the run, before the expensive work, so a typo in a scheduler file is a failed job rather than a quietly wrong export.
- Does adding that runtime check make the overloads pointless?No — they cover different populations. The stubs give checked call sites an exact return type and catch mistakes in CI before deploy; the runtime check covers everything the checker never saw, which is exactly where dynamic data enters. Keep both, and put the runtime check at the boundary where untyped input is parsed.
- Why did passing the config with ** defeat the stubs even though the caller was type-checked?Splatting a mapping hands the checker a `dict[str, str]` or `dict[str, Any]` rather than individual keyword arguments, so there is nothing to match against `Literal` stubs. Pass the arguments explicitly after parsing, and the stubs can do their job.
The stubs are a sign on the door listing who may enter; the implementation is the room. Nobody is standing at the door unless the body checks.
saying these in an interview costs you the question
- Expects an overload mismatch to raise at runtime
- Believes the checker validates values read from config files
- Puts validation code in an overload stub body
- Adds another overload stub instead of a runtime check
- Assumes Any-typed arguments are still checked at call sites
- Leaves a bare else standing for a named mode