Python Part I · Module 16 · Object-Oriented Programming
__init__ and self
Two pieces of syntax account for most of the confusion people have with Python classes: a method whose name is surrounded by double underscores and is never called directly, and a first parameter that every method must declare and no caller ever supplies. Both look like special cases bolted onto the language. Neither is. They follow from a single fact established two chapters ago — that the dot does something very specific — and once that connection is made, there is nothing left to memorise.
Intermediate16 min readPythonPart IFundamentalsOOPClasses
Part I · Module 16 · Chapter 16.3 · init and self
1. The Two Pieces
class TestRun:
def __init__(self, name, seed):
self.name = name
self.seed = seed
run = TestRun("smoke", 1234)
print(run.name, run.seed)smoke 1234The two questions this chapter answers:
__init__— why is the setup code in a method, with that name, that nobody calls?self— why does every method declare a parameter the caller never passes?
2. __init__ Is Called For You
TestRun("smoke", 1234) does two things: it creates an empty object, then it
calls __init__ on it with the arguments you supplied.
class TestRun:
def __init__(self, name, seed):
print(" __init__ running for", name)
self.name = name
self.seed = seed
print("about to create")
run = TestRun("smoke", 1234)
print("created:", run.name)
print("calling __init__ by hand:")
run.__init__("cdc", 5678)
print("now:", run.name, run.seed)about to create
__init__ running for smoke
created: smoke
calling __init__ by hand:
__init__ running for cdc
now: cdc 5678The last part is the point: __init__ is an ordinary method. You can call
it yourself and it behaves exactly as it did during creation. Nothing is special
about it except when Python calls it.
The double underscores mark it as a name Python looks for, rather than one you invent. Chapter 16.8 covers the rest of that family.
3. self Is the Instance, Arriving as Argument One
Chapter 16.1 Section 5 established that word.upper() is shorthand for
str.upper(word). That is the entire explanation of self:
class TestRun:
def __init__(self, name, seed):
self.name = name
self.seed = seed
def label(self):
return f"{self.name}/{self.seed}"
run = TestRun("smoke", 1234)
print("A dot form :", run.label())
print("B long form:", TestRun.label(run))
print("C same :", run.label() == TestRun.label(run))
print("D bound to :", run.label.__self__ is run)A dot form : smoke/1234
B long form: smoke/1234
C same : True
D bound to : TrueB is the same call written without the shorthand, and it makes self
visible: it is run, passed as the first positional argument.
D confirms it from the other side — run.label is a bound method that
already knows which instance it belongs to, and __self__ is that instance.
So def label(self): declares a parameter for the object, and run.label()
supplies it. The caller does not pass it because the dot already did.
4. __init__ Is a Normal Function in Every Other Respect
Because it is an ordinary method, everything Module 11 taught about parameters applies — defaults, keyword arguments, the lot:
class TestRun:
def __init__(self, name, seed=0, timeout_s=3600):
self.name = name
self.seed = seed
self.timeout_s = timeout_s
a = TestRun("smoke")
b = TestRun("cdc", 5678)
c = TestRun("burst", timeout_s=600)
for r in (a, b, c):
print(f"{r.name:<7} seed={r.seed:<5} timeout={r.timeout_s}")smoke seed=0 timeout=3600
cdc seed=5678 timeout=3600
burst seed=0 timeout=600Defaults (Chapter 11.5) and keyword arguments (Chapter 11.5) work unchanged. There is no separate "constructor syntax" to learn.
5. self Is Not a Keyword
The demonstration that settles it:
class TestRun:
def __init__(whatever, name):
whatever.name = name
def label(banana):
return f"label for {banana.name}"
run = TestRun("smoke")
print(run.name)
print(run.label())smoke
label for smokeIt works. self is an ordinary parameter name with no special status —
Python passes the instance as the first positional argument regardless of what
you called it.
So why does everyone write self? Because it is a universal convention, and
Chapter 13.3 Section 5 already gave the rule that decides such things: is this
a name your reader already knows, or one you invented? self is the most
firmly established convention in the language. Writing banana is legal, and
every reader of your code will stumble on it.
selfis a convention, not a keyword — and it is the one convention you should never break.
6. Failure One: Forgetting self in the Definition
class TestRun:
def __init__(self, name):
self.name = name
def label():
return "no self here"
run = TestRun("smoke")
print(run.label())Traceback (most recent call last):
File "noself.py", line 10, in <module>
print(run.label())
~~~~~~~~~^^
TypeError: TestRun.label() takes 0 positional arguments but 1 was givenRead the numbers: 0 declared, 1 given, and you passed none. The "1" is the instance, supplied by the dot.
Any TypeError whose counts are off by exactly one, on a method call with no
arguments, is this mistake. It is the most reliably diagnosable error in the
module.
7. Failure Two: Forgetting self. in the Body
This one is worse, because nothing fails at creation:
class TestRun:
def __init__(self, name):
name = name
run = TestRun("smoke")
print("created fine")
print(run.name)created fine
Traceback (most recent call last):
File "nodot.py", line 8, in <module>
print(run.name)
^^^^^^^^
AttributeError: 'TestRun' object has no attribute 'name'created fine printed. name = name is a legal statement — it rebinds the
local parameter to itself, does nothing useful, and stores nothing on the
object. The object is constructed successfully and is empty.
The failure arrives at the first use, which may be in another file, and the message says the class has no such attribute — which is true, and tells you nothing about the line that was supposed to set it.
self.x = xstores on the object.x = xstores nowhere.
This is Chapter 14.2 Section 5 again: the error surfaces where the missing value is used, not where it failed to be created.
8. Failure Three: Wrong Number of Arguments
class TestRun:
def __init__(self, name, seed):
self.name = name
self.seed = seed
run = TestRun("smoke")Traceback (most recent call last):
File "argcount.py", line 7, in <module>
run = TestRun("smoke")
TypeError: TestRun.__init__() missing 1 required positional argument: 'seed'The message names __init__ explicitly and names the missing parameter. Note it
does not count self among the missing — Python knows that one is supplied.
9. What self Is Not
Three clarifications that save time:
selfis not the class. It is one instance.TestRunis the class;selfis whichever object the method was called on.selfis not global. It exists only inside the method, as a parameter. Chapter 11.10's scope rules apply with no exceptions.selfdoes not have to be stored. A method can useselfto read attributes without assigning any.
class TestRun:
def __init__(self, name, seed):
self.name = name
self.seed = seed
def is_default_seed(self):
return self.seed == 0
a = TestRun("smoke", 0)
b = TestRun("cdc", 5678)
print("a:", a.is_default_seed())
print("b:", b.is_default_seed())
print("class is not an instance:", type(TestRun).__name__, "vs", type(a).__name__)a: True
b: False
class is not an instance: type vs TestRunThe last line is worth a second look: the class itself is an object too, and
its type is type. Everything really is an object, including the factories.
10. Predict, Run, Observe
class Counter:
def __init__(self, start=0):
self.value = start
def bump(self):
self.value += 1
return self.value
a = Counter()
b = Counter(10)
print("A:", a.value, b.value)
print("B:", a.bump(), a.bump(), b.bump())
print("C:", a.value, b.value)
print("D:", Counter.bump(a))
print("E:", a.value)
print("F:", a.bump.__self__ is a, a.bump.__self__ is b)A: 0 10
B: 1 2 11
C: 2 11
D: 3
E: 3
F: True FalseA — the default applied to a and not to b.
B — three calls, three separate counters advancing independently. Note the
arguments to one print are evaluated left to right, which is why a reads 1
then 2.
D is the long form from Section 3, passing the instance explicitly. It advanced the same counter, because it is the same call.
E confirms D mutated a rather than a copy.
F — a.bump is bound to a and to nothing else. Each instance has its own
bound version of the same underlying function.
11. Exercises
Exercise 1 — call it yourself. Create an object, then call __init__ on it
again by hand with different arguments. Confirm the attributes change.
Exercise 2 — the long form. Write a method and call it both as obj.m() and
as Class.m(obj). Confirm the results are identical.
Exercise 3 — rename self. Rewrite a working class using a different first parameter name throughout. Confirm it runs, then say why you would not ship it.
Exercise 4 — defaults. Give __init__ two defaulted parameters and create
three instances exercising different combinations.
Exercise 5 — the mutable default. Write def __init__(self, tags=[]),
create two instances, append to one's list and inspect the other's. Then fix it.
Exercise 6 — three failures. Produce all three errors from Sections 6, 7 and 8. For each, write the one-line diagnosis you would give from the message alone.
Exercise 7 — the silent one. Explain in two sentences why Section 7's mistake is more dangerous than Section 6's.
Exercise 8 — bound methods. For two instances of one class, show that
a.m.__self__ and b.m.__self__ differ. Say what that proves about where the
instance comes from.
12. Summary
__init__is an ordinary method that Python calls automatically after creating an instance; you can call it by hand.- The double underscores mark a name Python looks for, not one you invent.
selfis the instance arriving as the first positional argument, becauseobj.m()meansClass.m(obj).obj.mis a bound method that already knows its instance, visible as__self__.selfis not a keyword — any name works — but it is the one convention never to break.- Defaults and keyword arguments work normally in
__init__, including the mutable-default trap. - A method defined without
selfraises aTypeErroroff by exactly one. x = xinstead ofself.x = xraises nothing at creation and fails at first use with anAttributeError.- A missing constructor argument names
__init__and the parameter, and does not countself. - The class is itself an object, of type
type.
The next chapter looks at data that belongs to the class rather than to any one instance — and at the one way that distinction bites.
Where this fits
Part of the Python curriculum.
