skip to content

What does functools.partial return, and how are its stored arguments merged at call time?

level: juniorimportance: should knowfreq 42%

answer

  1. A callable object, not a function
  2. Stored arguments plus call arguments
  3. Positional ones go in front
  4. Caller's keywords beat the stored ones
  5. func, args and keywords attributes

basics

~20 s

functools.partial returns a new callable object that remembers the original callable plus the arguments you pre-bound. Calling it puts the stored positional arguments in front of the ones you pass and merges the stored keywords, which the caller can override.

solid answer

~40 s

`functools.partial(func, *args, **keywords)` does not call `func` and does not change it; it returns a `partial` object that stores the callable, a tuple of positional arguments and a dict of keyword arguments. Calling that object runs `func` with the stored positionals **prepended** to the call's positionals and the stored keywords merged with the call's keywords, where the caller's value wins. So a pre-bound positional can only be a leading prefix of the signature, while a pre-bound keyword behaves like a default the caller may replace. Passing a positional that lands on an already-bound keyword raises `TypeError: got multiple values for argument`. The result is an object, not a function: it has `.func`, `.args` and `.keywords` for introspection but no `__name__`, so logging code that reads `callback.__name__` breaks on it.

code

python · 15 lines
python
from functools import partial

def report(value, *, unit, digits=2):
    return f"{value:.{digits}f} {unit}"

as_mmol = partial(report, unit="mmol/L")
print(as_mmol(5.12345))              # 5.12 mmol/L
print(as_mmol(5.12345, digits=4))    # 5.1235 mmol/L
print(as_mmol.func, as_mmol.args, as_mmol.keywords)

def divide(a, b):
    return a / b

reciprocal = partial(divide, 1)      # freezes the FIRST parameter
print(reciprocal(4))                 # 0.25

go deeper

for a junior

Be ready to say in one sentence that functools.partial returns a new callable with some arguments already filled in, and that the original function is untouched. Knowing the stored positionals come first is enough at this level.

for a middle

Explain the merge mechanically: stored positionals are prepended, stored keywords are merged with the caller's winning, and a collision raises the wrapped function's own TypeError. Mention .func, .args and .keywords as the introspection surface.

for a senior

Show you know a partial is an object, not a function: no name, a readable repr, inspectable frozen state. That matters when partials flow into callback registries or logging code that assumes function attributes exist.

for a principal

Own the API-design angle: pre-bound keywords are defaults callers can override while pre-bound positionals are fixed, so choosing between them decides how much of your configuration a downstream caller can change.

## What it actually builds `functools.partial(func, *args, **keywords)` is an application of arguments that has been *deferred*. It does not invoke `func`, it does not mutate `func`, and it does not hand you back a function. It returns an instance of the `functools.partial` type, which stores exactly three things and is itself callable: * `.func` — the callable that will eventually run * `.args` — a tuple of the positional arguments you pre-bound * `.keywords` — a dict of the keyword arguments you pre-bound Those three attributes are the whole object. Anything you can predict about a partial's behaviour follows from how they are combined at call time. ## The merge rule When you call the partial with `*fargs, **fkeywords`, it effectively runs: ```python func(*stored_args, *fargs, **{**stored_keywords, **fkeywords}) ``` Read that carefully, because the two halves behave differently: **Positional arguments are prepended.** The stored tuple always comes first, and the call's positionals follow. That means a partial can only freeze a *leading prefix* of the parameter list. Given `def divide(a, b)`, `partial(divide, 1)` fixes `a`, and `half(4)` runs `divide(1, 4)`. There is no positional way to fix `b` and leave `a` open — to freeze a parameter that is not at the front, you must name it: `partial(divide, b=2)`. **Keyword arguments are merged, and the caller wins.** A pre-bound keyword is a *default*, not a lock. `report = partial(format_row, unit="mmol/L")` still lets `report(5.1, unit="mg/dL")` through, and the call's value replaces the stored one. This is deliberate and often useful, but it surprises people who expect pre-binding to be final. **Mixing the two can collide.** With `def divide(a, b)` and `p = partial(divide, 1)`, calling `p(a=9)` raises `TypeError: divide() got multiple values for argument 'a'` — the stored positional already filled `a`. Likewise, supplying more positionals than the signature accepts raises the ordinary "takes N positional arguments" `TypeError`. Nothing about a partial changes the underlying function's arity checking; the errors you get are the errors `func` itself would raise, because it *is* `func` raising them. ## It is an object, not a function This distinction shows up constantly in real code. A partial has no `__name__`, so `hasattr(p, "__name__")` is `False` and any framework, logger or dispatch table that identifies callbacks by `callback.__name__` raises `AttributeError` when you hand it one. Its `repr()` is `functools.partial(<function f at 0x...>, 1)`, which is genuinely informative in a traceback or a log line — you can see the target and the frozen arguments. `inspect.signature()` understands partials and reports the *remaining* parameters when the wrapped callable has an introspectable signature (it fails on builtins that expose none, exactly as it fails on those builtins directly). Because the state lives in plain attributes, a partial is transparent in a way a hand-written closure is not: given a registry of callbacks you can print `cb.func` and `cb.keywords` and see the configuration each one carries. That introspectability, not brevity, is the main reason to reach for it. ## Composition and nesting Partials compose: passing a partial as the `func` of another partial is allowed, and the argument prefixes apply in order, outermost binding first. They also drop naturally into any API that takes a callable — `sorted(rows, key=partial(...))`, `map(...)`, a `dict` of handlers, a scheduler's callback slot, an `iter(callable, sentinel)` loop. ## The mental model to keep Say it back as one sentence: *a partial stores a callable plus a prefix of positionals and a dict of keyword defaults, and calling it concatenates.* Concatenation explains the prefix restriction; dict merging with the caller on the right explains the override. Every other behaviour — the collision `TypeError`, the missing `__name__`, the readable `repr` — falls out of "it is an object holding three fields", not out of any special interpreter support.

  • How do you pre-bind a parameter that is not the first positional one?
    Bind it by name. Stored positionals are always prepended, so they can only fill a leading prefix of the signature; passing the argument as a keyword — `partial(divide, b=2)` — fixes any parameter the function is willing to accept by name. Positional-only parameters (those before a `/`) cannot be bound this way at all, so for those you must pre-bind everything up to and including the one you want.
  • What happens if you pass a positional argument that collides with something the partial already bound?
    You get the wrapped function's own `TypeError`. If the partial stored a positional that already fills `a` and you call it with `a=9`, Python raises `TypeError: got multiple values for argument 'a'`. If you pass more positionals than the signature accepts, you get the ordinary "takes N positional arguments" error. A partial adds no arity checking of its own — it concatenates and lets the target function complain.
  • Does a partial object expose the wrapped function's __name__ and __doc__?
    No. A `partial` has no `__name__`, so any code doing `callback.__name__` — a logger, a dispatch table, a decorator — raises `AttributeError`. Its `repr()` is `functools.partial(<function f at 0x...>, ...)`, which shows the target and the frozen arguments, and `.func` gives you the original callable if you genuinely need its name.

It is a form with some fields already filled in: handing it to someone still leaves the blank fields for them, and the typed-in defaults can be crossed out and rewritten, but the boxes filled at the top of the page stay where they are.

saying these in an interview costs you the question

  • Says partial calls the function immediately
  • Thinks partial mutates or rebinds the original function
  • Believes the stored positional arguments are appended, not prepended
  • Assumes a pre-bound keyword cannot be overridden by the caller
  • Expects the returned object to have __name__ like a function
  • Claims you can freeze any positional parameter, not just leading ones

context