skip to content

How does functools.partialmethod differ from functools.partial in a class body?

level: seniorimportance: nice to knowfreq 16%

answer

  1. Both are descriptors in a class
  2. The difference is where self lands
  3. Instance first, then the frozen arguments
  4. Plain partial appends the instance instead
  5. The binding behaviour changed in 3.14

basics

~20 s

functools.partialmethod is built for class bodies: it passes the instance as the first argument to the wrapped function, ahead of the pre-bound ones. Since Python 3.14 a plain functools.partial is also a method descriptor, but it inserts the instance after its stored arguments.

solid answer

~50 s

Both are descriptors in a class body, and the difference is *where the instance lands*. `functools.partialmethod(read, "utf-8")` produces a method whose call is `read(self, "utf-8", ...)` — the instance first, then the frozen arguments, which is what you want for a preconfigured method. A plain `functools.partial(read, "utf-8")` used as a class attribute follows partial's ordinary prepend rule, so it calls `read("utf-8", self, ...)` and the instance arrives in the wrong slot. That behaviour is new: `partial` became a method descriptor in Python 3.14. Before that, a partial in a class body was an ordinary attribute that never received the instance at all, and 3.13 emitted a `FutureWarning` telling you to wrap it in `staticmethod()` to keep the old behaviour. So the rule is simple — use `partialmethod` for methods, `partial` for free functions and callbacks — and code straddling 3.13 and 3.14 needs to be read carefully.

code

python · 14 lines
python
from functools import partial, partialmethod

class Reader:
    def _read(self, encoding, path):
        return f"self={type(self).__name__} encoding={encoding} path={path}"

    read_utf8 = partialmethod(_read, "utf-8")
    broken = partial(_read, "utf-8")

reader = Reader()
print(reader.read_utf8("rows.csv"))
# self=Reader encoding=utf-8 path=rows.csv
print(reader.broken("rows.csv"))
# self=str encoding=<Reader object ...> path=rows.csv

go deeper

for a junior

Recall only the rule of thumb: functools.partialmethod is the one to use inside a class body, functools.partial is for free functions and callbacks. You are unlikely to be asked more at this level.

for a middle

Explain the argument order. A partialmethod calls the wrapped function with the instance first and the frozen arguments after it, which is what makes the result behave like a method with some parameters already supplied.

for a senior

Show version awareness: partial became a method descriptor in Python 3.14 and inserts the instance after its stored arguments, so a class-body partial that used to fail loudly can now bind in a surprising order. Flag it when reviewing code moving across that boundary.

for a principal

Weigh it as a readability call. A generated family of preconfigured entry points can justify partialmethod, but for a few variants an explicit thin method is clearer, keeps a real name for tooling, and avoids a construct whose semantics just shifted under a release.

## The problem partialmethod exists to solve Pre-binding arguments to a *function* and pre-binding arguments to a *method* are not the same operation, because a method call inserts the instance. `functools.partialmethod` is the class-body counterpart of `functools.partial`, and the whole of the difference is argument order. Write a class with a general worker method and two preconfigured entry points: ```python class Reader: def _read(self, encoding, path): ... read_utf8 = partialmethod(_read, "utf-8") ``` Accessing `reader.read_utf8` produces a callable that runs `_read(reader, "utf-8", path)`. The instance goes in first, exactly where the `self` parameter is, and the frozen arguments follow. That is what makes the result usable as a method: the underlying function's signature is untouched, and subclasses can override `_read` and have both entry points follow. ## What a plain partial does there instead Since Python 3.14, `functools.partial` is itself a method descriptor, so a partial stored in a class body is also bound on attribute access. But it binds by partial's *own* rule, which is to prepend the stored arguments — the instance ends up appended after them. With `broken = partial(_read, "utf-8")`, calling `reader.broken("rows.csv")` runs `_read("utf-8", reader, "rows.csv")`: the string lands in `self`, the instance lands in `encoding`, and you get either a bizarre `TypeError` or, worse, a call that quietly succeeds with nonsense in the wrong parameters. This is a genuinely new hazard. Before 3.14, a `partial` in a class body was a plain attribute with no descriptor behaviour: `reader.broken("rows.csv")` never received the instance at all and typically failed with a missing-argument `TypeError`. Python 3.13 shipped a `FutureWarning` on that access announcing the coming change and telling you to wrap the partial in `staticmethod()` if you wanted the old non-binding behaviour. Anyone maintaining a codebase across that boundary should treat a partial in a class body as a thing to look at, not a thing to skim. ## Practical guidance The rule reduces to a sentence: **`partialmethod` in a class body, `partial` everywhere else.** If a preconfigured callable is going to be reached as `obj.name(...)`, it must be a `partialmethod`. If it is a free function, a callback in a registry, a `key=` argument, or anything handed to a pool, it is a `partial`. A few consequences worth carrying: * **Naming still differs from a normal method.** A `partialmethod` is not a function object; the accessed attribute has no `__name__` of its own, so tooling that identifies methods by name may need `.func`. * **Introspection is retained.** Like a partial, a `partialmethod` keeps its target and frozen arguments as public state, so the class documents its own preconfigured entry points. * **A hand-written method is often simpler.** `def read_utf8(self, path): return self._read("utf-8", path)` is three more characters and reads better to most teams. `partialmethod` earns its place when you are generating a family of entry points programmatically, or when the frozen argument list is long enough that repeating it invites drift. * **Overriding still works normally.** Because both forms ultimately call a named method on the class, subclass overrides of the underlying method take effect through the preconfigured entry point. ## Answering it well An interviewer asking this is probing whether you understand the descriptor protocol well enough to predict where `self` goes, and whether you track version-sensitive behaviour. Lead with "both are descriptors now, they differ in where the instance is inserted", give the two argument orders explicitly, then note that `partial` only became a method descriptor in 3.14 and that 3.13 warned about it. That is the complete answer, and the version detail is what separates a rehearsed one from a current one.

  • What did a functools.partial stored in a class body do before Python 3.14?
    Nothing special — it was an ordinary class attribute with no descriptor behaviour, so accessing it through an instance returned the partial itself and the instance was never passed. Calls usually failed with a missing-argument `TypeError`. Python 3.13 emitted a `FutureWarning` on that access announcing the change and suggesting `staticmethod()` for code that relied on the old behaviour.
  • When would you write a plain method instead of a functools.partialmethod?
    Almost always, for a handful of entry points: `def read_utf8(self, path): return self._read("utf-8", path)` is clearer to most readers and gives you a real function with a name and a docstring. `partialmethod` pays off when the preconfigured variants are generated programmatically or the frozen argument list is long enough that hand-written duplicates would drift apart.
  • Does a subclass overriding the underlying method change what the partialmethod calls?
    Yes. The `partialmethod` records the function it was given at class-definition time, but the ordinary rule is that you build it over a method the class owns; if a subclass overrides that method, code that reaches the worker through `self` follows the override. This is one reason preconfigured entry points are usually written as thin methods that call `self._worker(...)`.

saying these in an interview costs you the question

  • Thinks partial and partialmethod are interchangeable in a class
  • Cannot say where the instance is inserted
  • Assumes a class-body partial never binds on Python 3.14
  • Believes partialmethod calls the function immediately at class creation
  • Unaware that partial's class-body behaviour changed in 3.14

context