Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class TestRun:
    def __init__(self, name, seed):
        self.name = name
        self.seed = seed


run = TestRun("smoke", 1234)
print(run.name, run.seed)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke 1234

The two questions this chapter answers:

  1. __init__ — why is the setup code in a method, with that name, that nobody calls?
  2. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
about to create
  __init__ running for smoke
created: smoke
calling __init__ by hand:
  __init__ running for cdc
now: cdc 5678

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A dot form : smoke/1234
B long form: smoke/1234
C same      : True
D bound to  : True

B 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke   seed=0     timeout=3600
cdc     seed=5678  timeout=3600
burst   seed=0     timeout=600

Defaults (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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
smoke
label for smoke

It 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.

self is a convention, not a keyword — and it is the one convention you should never break.

6. Failure One: Forgetting self in the Definition

Azvya Education Pvt. Ltd.VLSI Mentor
noself.py (BROKEN ON PURPOSE)
class TestRun:
    def __init__(self, name):
        self.name = name

    def label():
        return "no self here"


run = TestRun("smoke")
print(run.label())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 given

Read 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:

Azvya Education Pvt. Ltd.VLSI Mentor
nodot.py (BROKEN ON PURPOSE)
class TestRun:
    def __init__(self, name):
        name = name


run = TestRun("smoke")
print("created fine")
print(run.name)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 = x stores on the object. x = x stores 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

Azvya Education Pvt. Ltd.VLSI Mentor
argcount.py (BROKEN ON PURPOSE)
class TestRun:
    def __init__(self, name, seed):
        self.name = name
        self.seed = seed


run = TestRun("smoke")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

  • self is not the class. It is one instance. TestRun is the class; self is whichever object the method was called on.
  • self is not global. It exists only inside the method, as a parameter. Chapter 11.10's scope rules apply with no exceptions.
  • self does not have to be stored. A method can use self to read attributes without assigning any.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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__)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a: True
b: False
class is not an instance: type vs TestRun

The 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 0 10
B: 1 2 11
C: 2 11
D: 3
E: 3
F: True False

A — 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.
  • self is the instance arriving as the first positional argument, because obj.m() means Class.m(obj).
  • obj.m is a bound method that already knows its instance, visible as __self__.
  • self is 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 self raises a TypeError off by exactly one.
  • x = x instead of self.x = x raises nothing at creation and fails at first use with an AttributeError.
  • A missing constructor argument names __init__ and the parameter, and does not count self.
  • 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.