functools.partial Objects With Identical Args Are Never Equal (tested)

A functools.partial object exposes its wrapped function and bound arguments as plain, inspectable attributes (.func, .args, .keywords), which looks like a value object -- something equal data should make equal. We measured that it isn't: partial defines no __eq__ or __hash__, so it falls back to identity, and two partials built from identical inputs are simply two different objects to Python.

1. Same function, same args, not equal

import functools

def f(a, b):
    return a + b

p1 = functools.partial(f, 1, 2)
p2 = functools.partial(f, 1, 2)

print("p1 == p2:", p1 == p2)
print("p1.func is p2.func:", p1.func is p2.func)
print("p1.args == p2.args:", p1.args == p2.args)
print("hash(p1) == hash(p2):", hash(p1) == hash(p2))
p1 == p2: False
p1.func is p2.func: True
p1.args == p2.args: True
hash(p1) == hash(p2): False

Measured: every piece of data inside the two partials is identical -- same wrapped function object, same argument tuple -- yet == is False and the hashes differ. functools.partial inherits no __eq__ or __hash__ from its data, so Python's default identity-based comparison applies: two instances are equal only if they're literally the same object in memory.

2. A set or dict built to deduplicate callbacks silently keeps both

import functools

def f(a, b):
    return a + b

p1 = functools.partial(f, 1, 2)
p2 = functools.partial(f, 1, 2)

callbacks = {p1, p2}
print("set length:", len(callbacks))

registry = {p1: "handler A"}
registry[p2] = "handler B"
print("dict length:", len(registry))
print(registry[p1], "/", registry[p2])
set length: 2
set length: 2
dict length: 2
handler A / handler B

Measured: no exception, no warning -- the set and the dict simply hold two entries where code that assumes value-based equality expects one. registry[p1] and registry[p2] stay as two completely independent slots even though, from the caller's point of view, they represent "the same callback". A pattern like "register this callback only if an equal one isn't already registered" silently does nothing to prevent duplicates with partial objects.

3. Working around it

import functools

def f(a, b):
    return a + b

p1 = functools.partial(f, 1, 2)
p2 = functools.partial(f, 1, 2)

def partial_key(p):
    return (p.func, p.args, tuple(sorted(p.keywords.items())))

seen = {partial_key(p1), partial_key(p2)}
print("deduped length:", len(seen))
deduped length: 1

Measured: building an explicit key tuple from .func, .args and .keywords and deduplicating on that tuple (which does use value equality) collapses the two partials to one, as expected. There is no built-in flag or subclass that does this automatically -- it has to be done by hand at every call site that needs it.

Frequently asked questions

Are two functools.partial objects with the same function and arguments equal?

No. We measured two partial(f, 1, 2) objects built independently: == returned False and their hashes differed, even though .func, .args and .keywords were all identical, because partial defines no __eq__ or __hash__ and falls back to identity comparison.

Will a set or dict deduplicate functools.partial objects with identical arguments?

No. We measured putting two value-identical partial objects into a set and using them as dict keys: both ended up as separate entries, with no error raised, because deduplication relies on == and hash, which partial does not define in terms of its wrapped data.

How do I deduplicate functools.partial callbacks by their function and arguments?

Build an explicit comparison key from the partial's own attributes, such as (p.func, p.args, tuple(sorted(p.keywords.items()))), and deduplicate on that tuple instead of on the partial object itself -- we measured this correctly collapsing two value-identical partials to one.

Is this specific to functools.partial, or true of other callables too?

It follows from partial not implementing __eq__/__hash__ based on its contents -- the same default-identity behavior applies to any plain Python object that doesn't define those methods, such as a plain function wrapper or a simple class instance with no __eq__.

Written with AI assistance. Measured on 2026-10-08 with Python 3.13.5's built-in functools.partial: created two separate partial objects wrapping the same function with identical positional arguments, then compared them with ==, hashed them, and put both into a set and a dict to see whether either deduplicated them. PlainHeaders is our extension; Python's functools module is built in.