Why does reaching a plain function through an instance produce a bound method in Python?
answer
- No method type exists at def time
- The function type implements one dunder
- Access, not definition, does the work
- __func__ plus __self__
- Non-data, so an instance value shadows it
basics
~20 sFunction objects implement get, making every function a non-data descriptor. Reaching one through an instance calls that get, which returns a bound method object pairing the function with the instance so the first parameter is supplied automatically.
solid answer
~50 sThere is no separate 'method' declaration in Python: `def` inside a class body creates an ordinary function stored in the class `__dict__`. The function's *type* defines `__get__`, so attribute access on an instance goes through the descriptor protocol and returns a **bound method** — a small object holding `__func__` (the function) and `__self__` (the instance) that prepends the instance when called. Reaching the same name through the class gives back the plain function, because `__get__` is called with `obj=None` and simply returns the function unchanged. `staticmethod` is a descriptor whose `__get__` returns the underlying function without binding anything; `classmethod` binds to the class instead of the instance. And because functions are *non-data* descriptors, a callable stored in an instance's `__dict__` shadows the method and is returned unbound, which is why an instance-level patch never receives `self`.
code
python · 14 linesclass Collector:
def sample(self):
return "reading"
c = Collector()
func = Collector.__dict__["sample"]
print(type(func)) # <class 'function'>
print(type(c.sample)) # <class 'method'>
print(func.__get__(c, Collector) == c.sample) # True
print(c.sample.__self__ is c) # True
print(c.sample.__func__ is func) # True
print(c.sample is c.sample) # False: bound fresh each accessgo deeper
Recall that self is not magic: it is the ordinary first parameter, supplied because the method was fetched from an instance. Know that Class.method and instance.method are not the same object.
Explain the mechanism: functions implement get, so access returns a bound method carrying func and self, and class access with obj as None returns the plain function.
Bring the operational consequences: bound methods pin their instance in callback registries, instance-level callables never receive self, and patching on the class affects every object at once.
Frame it as a policy question for the codebase: where per-object substitution is a supported extension point versus an accident, and whether callback registries should hold weak references so component lifetimes stay predictable.
### There is no method type at definition time A `def` in a class body executes like any other `def`: it builds a function object and binds it to a name in the class namespace. Nothing marks it as a method. What makes it *behave* like one is that the built-in function type implements `__get__`, so a function is a non-data descriptor by construction. ```python class Collector: def sample(self): return "reading" type(Collector.__dict__["sample"]) # <class 'function'> type(Collector().sample) # <class 'method'> ``` The class dictionary holds a function. Only the *access* produces a method. ### What binding actually produces `instance.sample` runs `object.__getattribute__`, which finds the function on the type and calls `function.__get__(instance, Collector)`. That returns a **bound method object** with two attributes: * `__func__` — the original function object, shared by every binding; * `__self__` — the instance the method was fetched from. Calling the bound method inserts `__self__` as the first positional argument and delegates to `__func__`. So `c.sample()` is `type(c).sample(c)` with a small wrapper in between, and the parameter conventionally named `self` is not magic at all — it is the ordinary first parameter, filled in by the binding step. The binding is also *fresh each time*. Two accesses to `c.sample` produce two distinct bound method objects, equal to each other (bound methods compare by `__func__` and `__self__`) but not identical. This is why using a bound method as a dictionary key or storing it in a callback registry keeps the instance alive: the bound method holds a strong reference to `__self__`, which is a real source of retention in long-lived collector or observer registries. `weakref.WeakMethod` exists exactly for that case. ### Class access, and the variants When the same attribute is reached through the class, `obj` is `None`, and a function's `__get__` returns `self` — the plain function. That is why `Collector.sample` is a function you must call with an explicit instance, and why `Collector.sample(c)` and `c.sample()` do the same thing. Python 2's "unbound method" wrapper for this case was removed in Python 3; the class attribute is simply the function. The two familiar decorators are ordinary descriptors written in C over the same protocol: * `staticmethod` wraps a function and its `__get__` returns that function **unbound**, so no instance or class is prepended. Since 3.10 the wrapper is itself directly callable, so calling one out of the class dictionary also works. * `classmethod`'s `__get__` binds to the owning class rather than the instance, so `__self__` on the result is the class object. Reaching it through an instance still binds the class, and through a subclass binds the subclass — which is what makes alternative-constructor patterns inherit correctly. ### The failure this explains Because functions are **non-data** descriptors, they lose to the instance dictionary. Assigning a callable to an instance attribute therefore stores it raw: ```python c.sample = lambda: "stubbed" c.sample() # works, but no instance was passed ``` The stored lambda is returned straight out of `obj.__dict__` with no `__get__` call, so it receives no `self`. Anyone hand-rolling a per-object stub for a sensor-telemetry collector hits this immediately: the replacement must either take no `self` parameter, or be bound explicitly with `func.__get__(instance, type(instance))`, or be installed on the class instead. The mirror-image mistake is patching on the class and forgetting that every instance now sees it. ### Performance, and what changed Materialising a bound method object on every call would be wasteful, and CPython avoids it. Since 3.11 the specializing adaptive interpreter recognises the fetch-then-call pattern at a call site and, when the shape is stable, calls the underlying function with the instance pushed as the first argument without allocating the intermediate bound method at all. The semantics are unchanged — you still get a real method object the moment you store the attribute in a variable or pass it somewhere — but the common `c.sample()` path is much cheaper than the description above suggests. One 3.14 refinement worth knowing: `functools.partial` now implements the descriptor protocol too, so a partial object placed in a class body binds like a function does. On earlier versions it did not, and a partial used as a method silently failed to receive the instance.
- How do staticmethod and classmethod differ from a plain function in this machinery?Both are descriptor types wrapping a function. staticmethod's __get__ hands back the wrapped function with nothing prepended, so it behaves identically through the class or an instance. classmethod's __get__ binds the owning class, so __self__ on the result is the class object, and reaching it through a subclass binds that subclass. A plain function binds the instance, or returns itself when reached through the class.
- Why can holding a bound method in a callback list keep an object alive?Because the bound method stores a strong reference to the instance in __self__. A registry of callbacks that holds bound methods therefore pins every instance it was given, which shows up as steadily growing memory in long-lived services. weakref.WeakMethod wraps a bound method without extending the instance's lifetime, and is the standard fix for observer and callback registries.
- Is a new bound method object created on every attribute access?Semantically yes, which is why c.sample is c.sample is False while c.sample == c.sample is True. In practice CPython's specializing interpreter has, since 3.11, been able to skip the allocation when the attribute is fetched and immediately called at a stable call site. Store the attribute in a variable and you get a real method object again.
The function is a form letter kept in the class filing cabinet; fetching it through an instance hands you a copy with the addressee already filled in.
saying these in an interview costs you the question
- Says def in a class body creates a special method object
- Thinks self is inserted by interpreter magic rather than by binding
- Expects Class.method to return an unbound method wrapper
- Assumes a lambda assigned to an instance receives self
- Believes each instance gets its own copy of every method