skip to content

In a result-loading pipeline built on functools.partial callbacks, why can a caller override a pre-bound keyword, and how do you prevent it?

level: seniorimportance: should knowfreq 28%

answer

  1. Pre-binding is not sealing
  2. Two merges behave differently
  3. Later keys win the dict merge
  4. Positional collisions raise, keyword ones do not
  5. Use a slash for positional-only

basics

~20 s

A keyword pre-bound with functools.partial is a default, not a lock: the call's keywords are merged over the stored ones, so the caller wins. Bind the value positionally, or make the parameter positional-only, if it must not be overridable.

solid answer

~50 s

`functools.partial` merges its stored keywords with the ones passed at the call, and the call's value takes precedence — the stored dict is on the left of the merge. So `partial(load_rows, encoding="utf-8")` publishes a *default* encoding; any caller passing `encoding="cp1252"` silently replaces it, and the mismatch only surfaces later as mojibake in the decoded values. Positional pre-binding behaves oppositely: a stored positional already fills the parameter, so a caller who names it gets `TypeError: got multiple values for argument`. The hardening options follow from that. Bind the value positionally where the signature allows it; declare the parameter positional-only with `/` so callers cannot name it at all; or wrap the target in a small module-level function that takes no such parameter. Whichever you pick, log the resolved configuration — `.func` and `.keywords` on the partial make the frozen state inspectable in the registry.

code

python · 18 lines
python
from functools import partial

def load_soft(path, *, encoding):
    return f"{path} decoded as {encoding}"

reader = partial(load_soft, encoding="utf-8")
print(reader("results.csv"))
print(reader("results.csv", encoding="cp1252"))   # silently overridden

def load_hard(encoding, /, path):
    return f"{path} decoded as {encoding}"

locked = partial(load_hard, "utf-8")
print(locked("results.csv"))
try:
    locked("results.csv", encoding="cp1252")
except TypeError as exc:
    print("TypeError:", exc)

go deeper

for a junior

Recall the direction of the merge: when a call passes the same keyword a functools.partial already bound, the call's value is the one that reaches the function. Pre-binding sets a default, not a lock.

for a middle

Explain both halves of the merge and why they differ: stored positionals fill slots and collide loudly with a caller keyword, while stored keywords are merged with the caller's on top. Name the positional-only / fix.

for a senior

Show the production instinct: an invariant frozen as an overridable keyword is a silent-corruption bug, so encode it positional-only, and log the resolved configuration rather than the registered one so the next mismatch is diagnosable at the call site.

for a principal

Own the API contract. Decide per argument whether it is a default callers may adjust or an invariant they may not, and make the signature carry that decision so every future registry built on it inherits the guarantee without a review catching it.

## The scenario A clinical-lab result loader keeps a registry of per-format reader callbacks, each built as `partial(load_rows, encoding="utf-8")` with some analyzer-specific configuration frozen in. One caller — a re-import path for an older analyzer — passes `encoding="cp1252"` of its own. Nothing raises. The rows load, patient result fields come back with mangled characters, and the encoding mismatch is caught only by a 27-minute end-to-end suite hours later, far from the line that caused it. The question an interviewer is really asking is: did you know that could happen, and what do you do about it? ## Why the override is possible The merge rule inside a partial is asymmetric, and that asymmetry is the entire answer: * **Stored positionals are prepended.** They occupy their parameter slots before the caller's arguments are considered, so they are effectively fixed. * **Stored keywords are merged with the caller's on top.** The equivalent of `{**stored_keywords, **call_keywords}` — later keys win, and the caller's are later. So `partial(f, encoding="utf-8")` does not *bind* the encoding, it *defaults* it. This is intentional and frequently what you want: it is how you build a preconfigured callable that still lets a specific call opt out. It becomes a defect only when you assumed the opposite and treated the partial as a sealed configuration. The contrast is sharp and worth stating explicitly in an interview: with `def load_rows(encoding, path)` and `reader = partial(load_rows, "utf-8")`, a caller doing `reader("x.csv", encoding="cp1252")` gets `TypeError: got multiple values for argument 'encoding'`. The exact same intent, expressed positionally, fails loudly instead of silently. ## Hardening, from cheapest to strongest **1. Bind positionally.** If the parameter is at the front of the signature, freeze it as a positional argument. The collision then raises immediately at the call site. The limitation is the prefix rule — you can only pre-bind a leading run of parameters this way, so this works when you control the signature and can order it deliberately. **2. Make the parameter positional-only.** Writing `def load_rows(encoding, /, path)` means callers *cannot* name `encoding` at all; attempting it raises `TypeError: got some positional-only arguments passed as keyword arguments`. This is the strongest and most self-documenting option, because the signature itself declares which arguments are the factory's to set and which are the caller's. It is the right move when the same target is reachable both directly and through the registry. **3. Wrap instead of partial.** A module-level `def read_analyzer_a(path): return load_rows("utf-8", path)` removes the parameter from the public surface entirely. It is more code, it stays picklable, and it costs you the introspectable `.keywords`. **4. Validate at the boundary.** If the parameter must remain overridable for legitimate reasons, make the override explicit rather than silent: have the target reject unknown or non-allowlisted values, or record the resolved value in the row batch's provenance so a mismatch is attributable when it surfaces. ## The operational half The reason this reaches senior level is the diagnosis, not the syntax. When a mismatch shows up, the partial is your friend: the frozen state is public, so a registry health check can print `cb.func`, `cb.args` and `cb.keywords` for every entry and show what each reader is actually configured with. A capturing closure gives you nothing comparable. Log the *resolved* configuration at call time rather than the registered one — the whole failure is that those two can differ — and the next occurrence is a one-line diagnosis instead of a 27-minute suite run. ## The judgement to state out loud There is no universally right choice. Overridable keywords are a feature when the registry supplies sensible defaults for a diverse set of callers; they are a hazard when the frozen value is a correctness invariant, as an encoding or a unit system is. Decide which one each pre-bound argument is, and encode that decision in the signature — positional-only for invariants, keyword for defaults — instead of leaving it to a comment.

  • Why does binding the same value positionally fail loudly instead of silently?
    Because stored positionals are prepended into the parameter slots before the caller's arguments are applied. When the caller then names that parameter, two values target it and the wrapped function raises `TypeError: got multiple values for argument`. Keywords never collide that way — they go through a dict merge in which the caller's entry simply replaces the stored one.
  • When is an overridable pre-bound keyword the behaviour you actually want?
    Whenever the frozen value is a convenience default rather than a correctness invariant: a page size, a timeout, a retry count, a verbosity level. A preconfigured callable that still lets one call opt out is exactly what partial is designed for. The defect is not overridability itself, it is overridability on a value the rest of the system assumes is fixed.
  • How would you inspect a registry of partial callbacks to find a misconfigured one?
    Iterate the registry and read the public state on each entry: `.func` for the target, `.args` and `.keywords` for the frozen configuration, and `repr()` for a one-line summary. Because the fault is a divergence between registered and resolved configuration, also log the effective value at call time — the registered snapshot alone will look correct.

saying these in an interview costs you the question

  • Believes pre-binding a keyword makes it immutable
  • Says the stored keyword wins over the caller's
  • Cannot name a way to make the value non-overridable
  • Treats positional and keyword pre-binding as equivalent
  • Proposes documenting the hazard instead of encoding it in the signature
  • Never inspects the frozen state on the registered callables

context