2.1.5 · D5OOP Fundamentals
Question bank — `self` — what it is and how Python passes it
Prerequisites this bank pokes at: Classes and Instances, __init__ — the constructor, Instance vs Class Attributes, Bound and Unbound Methods, Descriptor Protocol, and classmethod and staticmethod.
True or false — justify
self is a reserved keyword in Python, like def or return.
False — it is only a naming convention.
def f(banana): banana.x = 1 works identically; the first parameter receives the instance no matter what you name it.Renaming self to this in one method will break the class.
False — the name is local to that method. Python fills the first slot with the instance regardless;
this.balance inside works fine (though it will confuse human readers).obj.method() and Class.method(obj) do exactly the same thing.
True — the dot on an instance is sugar that inserts
obj as the first argument, so both call the same function body with self = obj.Every method you define automatically has access to self even if you forget to list it.
False — you must declare it in the signature. If you write
def greet(): the instance still arrives as arg 1, causing TypeError: takes 0 positional arguments but 1 was given.self stores a copy of the object's data.
False —
self is a reference to the very same object. Mutating self.balance changes the original instance; there is no copy.Two instances of the same class share the same self.
False — each call gets the instance it was invoked on.
a.inc() binds self=a; b.inc() binds self=b. Same code, different self, separate state.Inside a method, writing balance = 5 sets the instance's balance attribute.
False — that creates a local variable that vanishes when the method returns. To touch the instance you must write
self.balance = 5.alice.deposit and Account.deposit are the same object.
False —
alice.deposit is a bound method (function + instance packaged), while Account.deposit is the raw function. They compare unequal and print differently.Returning self from a method is required for the method to work.
False — it is optional and only enables method chaining (
box.add(1).add(2)). A method that mutates and returns None is perfectly valid.The value of self is decided at method-definition time.
False — it is decided at call time, by whatever instance sits to the left of the dot (or is passed explicitly).
Spot the error
class C:
def greet():
print("hi")
C().greet()
``` ::: Missing `self` in the signature. The dot still passes the instance, so a 0-parameter function receives 1 argument → `TypeError`. Fix: `def greet(self):`.
```python
alice.deposit(alice, 50)
``` ::: Passing the instance twice. The dot already supplied `alice` as `self`; adding it again gives too many arguments. Fix: `alice.deposit(50)`.
```python
def deposit(self, amount):
balance += amount
``` ::: Bare `balance` is treated as a local variable that was never assigned → `UnboundLocalError`/`NameError`. Fix: `self.balance += amount`.
```python
class Box:
def __init__(self):
self.items = []
def add(self, x):
self.items.append(x)
b = Box().add(1).add(2)
``` ::: `add` returns `None` (no explicit return), so the second `.add(2)` is `None.add(2)` → `AttributeError`. Fix: end `add` with `return self`.
```python
class Timer:
def start():
self.t = 0
``` ::: Two problems: `self` isn't in the signature, **and** the body uses `self` which isn't in scope. The instance never gets a name to bind to. Fix: `def start(self): self.t = 0`.
```python
class Dog:
def bark(self):
print(name)
d = Dog()
d.name = "Rex"
d.bark()
``` ::: `name` is looked up as a local/global, not on the instance → `NameError`. The attribute lives on the object, so it must be `self.name`.
---
## Why questions
Why does the "takes 0 positional arguments but 1 was given" error appear when you *called with no arguments*? ::: Because the dot silently supplied the instance as argument 1. You passed nothing explicitly, but Python did — and the function had no slot for it.
Why must the method know *which* instance it operates on at all? ::: A class is one shared blueprint used by many objects. The single method body must edit `alice`'s data on one call and `bob`'s on another; `self` carries that "which one".
Why does `alice.deposit` "remember" `alice` even before you call it? ::: Because functions are **descriptors**: accessing one through an instance triggers the [[Descriptor Protocol]], which returns a bound method holding the instance in `__self__`.
Why is `self` conventionally always the *first* parameter and not, say, the last? ::: Because the dot injects the instance as the **first positional argument**. Any other position wouldn't line up with what Python passes.
Why can you simulate `self` with plain functions and dicts? ::: Because "automatic `self`" is just manual passing in disguise. `deposit(alice, 50)` on a dict does exactly what `alice.deposit(50)` does on a class — the dot only saves you from typing the object.
Why doesn't Python just use a hidden global for "the current object" instead of an argument? ::: A global would break with nested calls, threads, and reentrancy — two objects' methods could clash. Passing `self` explicitly keeps every call's "which object" independent and unambiguous.
Why does method chaining need `return self` specifically, not `return self.items`? ::: The next call in the chain must land on the **same object** that has the methods. Returning the list would let you call list methods, not the class's methods.
---
## Edge cases
What is `self` inside a [[classmethod and staticmethod|staticmethod]]? ::: There is none — a `@staticmethod` gets **no** automatic first argument. The dot does not inject the instance, so it behaves like a plain function living in the class's namespace.
What replaces `self` in a [[classmethod and staticmethod|classmethod]]? ::: `cls` — the dot injects the **class object**, not the instance. So `cls` lets you touch class-level data or build new instances, not per-instance state.
If you call `Account.deposit(42, 50)` where `42` is not an `Account`, what happens? ::: It runs — `self` is just a name, so `self=42`. It only fails when the body does something `42` can't do (e.g. `self.balance += ...` raises `AttributeError`). Python does **not** type-check `self`.
Can `self` ever be `None`? ::: Yes, if you explicitly pass it: `Account.deposit(None, 50)` sets `self=None`. Normal dot-calls never produce this, but the unbound form allows any object.
Does a bound method keep the instance alive? ::: Yes — `alice.deposit` stores `alice` in `__self__`, so holding the bound method keeps a reference to `alice`, preventing it from being garbage-collected.
What does `type(alice).__dict__['deposit']` give versus `alice.deposit`? ::: The first is the **raw function** stored on the class; the second is the **bound method** produced on access. Same underlying `__func__`, different wrappers.
If a subclass overrides a method, which body runs when the base class's `__init__` calls `self.method()`? ::: The **subclass's** version — `self` is the actual (sub)instance, so attribute lookup starts from its real type, not the class where the call textually appears.
---
## Recall
> [!recall]- One-sentence summary of every trap above
> `self` is a plain first argument the dot fills with the instance at *call time* — it is a reference (not a copy), a convention (not a keyword), must be declared and prefixed on every attribute, and its presence/absence is exactly what separates instance methods from static and class methods.
---
## Connections
- [[OOP Fundamentals]]
- [[Classes and Instances]]
- [[__init__ — the constructor]]
- [[Instance vs Class Attributes]]
- [[Bound and Unbound Methods]]
- [[Descriptor Protocol]]
- [[classmethod and staticmethod]]