functools.partialmethod Binds One Shared Mutable Object to Every Instance (tested)
The classic Python trap is a mutable default argument in a def signature, like def f(x=[]) -- experienced developers know to avoid it. functools.partialmethod can create the exact same hazard in a form that doesn't look like a function default at all: a dict or list literal passed as a keyword argument to partialmethod() is evaluated once, when the class body runs, and that one object is bound into every instance's call.
1. Two instances, one shared dict
import functools
class Widget:
def configure(self, options={}):
options["touched"] = True
return options
set_default = functools.partialmethod(configure, options={})
w1 = Widget()
w2 = Widget()
r1 = w1.set_default()
r2 = w2.set_default()
print("r1 is r2:", r1 is r2)
print("r1:", r1)
print("r2:", r2)
r1 is r2: True
r1: {'touched': True}
r2: {'touched': True}
Measured: w1.set_default() and w2.set_default() -- two different Widget instances, two separate calls -- return the exact same dict object. The options={} literal inside functools.partialmethod(configure, options={}) runs exactly once, while the class body executes, and that single dict is stored as part of the partialmethod descriptor itself. Every instance's call passes that same object through to configure(), which is free to mutate it.
2. A mutation in one call is visible from a completely unrelated instance
import functools
class Basket:
def add(self, item, items=[]):
items.append(item)
return items
add_default = functools.partialmethod(add, items=[])
b1 = Basket()
b2 = Basket()
b1.add_default("apple")
print("b2's basket already has:", b2.add_default("banana"))
b2's basket already has: ['apple', 'banana']
Measured: b2 never touched b1, yet calling b2.add_default("banana") returns a list that already contains "apple" from an earlier call on b1. No exception, no warning -- the list keyword argument baked into the partialmethod is one object, shared for the lifetime of the class, across every instance that ever calls it.
3. The fix: bind nothing mutable, build it inside the method instead
import functools
class Basket:
def add(self, item, items=None):
if items is None:
items = []
items.append(item)
return items
add_default = functools.partialmethod(add, items=None)
b1 = Basket()
b2 = Basket()
b1.add_default("apple")
print("b2's basket:", b2.add_default("banana"))
b2's basket: ['banana']
Measured: binding items=None through the partialmethod and constructing the actual list inside the method body -- the same fix used for ordinary mutable default arguments -- gives each call its own fresh list. The hazard isn't specific to partialmethod's mechanics; it's the same "mutable default evaluated once" trap as a plain function signature, just moved somewhere a signature-mutable-default code review wouldn't think to look.
Frequently asked questions
Does functools.partialmethod share a mutable keyword argument across instances?
Yes. We measured partialmethod(configure, options={}) on a class: calling the resulting method from two completely separate instances returned the exact same dict object (confirmed with `is`), because the {} literal is evaluated once when the class body runs, not once per call.
Why is this surprising if mutable default arguments are already a known Python pitfall?
Because it doesn't look like a default argument at all -- there's no `def f(x={})` signature to spot in review. The hazard is hidden inside a functools.partialmethod() call, which reads like simple argument currying, not like something that evaluates a mutable literal once and keeps it alive for the life of the class.
How do I avoid functools.partialmethod binding a shared mutable object?
Bind an immutable sentinel such as None through partialmethod, and construct the actual mutable object (list, dict, etc.) inside the method body when the argument is None -- we measured this giving each call an independent object instead of a shared one.
Does this affect functools.partial the same way as partialmethod?
Yes, the same mechanism applies to functools.partial on a plain function -- any mutable object passed as an argument to partial() or partialmethod() is evaluated once at that call site and reused on every subsequent call through the resulting partial.
Written with AI assistance. Measured on 2026-10-08 with Python 3.13.5's built-in functools.partialmethod: defined a class attribute as partialmethod(method, kwarg={}), created two separate instances, called the partialmethod on each, and checked with `is` whether the two calls received the same dict object. PlainHeaders is our extension; Python's functools module is built in.