Course outline · 0% complete

0/27 lessons0%

Course overview →

Classes and __init__

lesson 3-1 · ~13 min · 7/27

A reminder before we start: the mutable default trap

Recalling lesson 2-2, the problem with def register(user, users=[]) is that the list default is created once, so every call shares one list.

The [] is built a single time, when the def line runs, not on each call. The fix is to default to None and create the list inside the body. This is worth having fresh in mind, because class attributes have a similar sharing gotcha later in this unit.

It is not standard, harmless Python, lists have no trouble holding strings, and the parameter order is fine. The sharing is the whole issue.

When a dict is not enough

Almost every large Python codebase, from web frameworks to data libraries to the standard library itself, is organized around classes, so this unit is where your code starts to look like a working engineer's. The motivation is concrete.

In Python for Beginners you modeled things with dicts: player = {"name": "mia", "score": 0}. That works right up until there is behavior that belongs to the data. The code that levels a player up has nowhere natural to live. You end up with loose functions that each take a player dict, spread across the file, and nothing ties them to the shape of data they assume.

A class bundles data and behavior into one unit. It is a blueprint. Each value stamped from the blueprint is an object (also called an instance) with its own data:

class Player:
    def __init__(self, name):
        self.name = name
        self.score = 0

__init__ is the initializer: Python calls it automatically when you write Player("mia"). self is the object being created, and self.name = name stores data on that object as an attribute.

class Playerthe blueprintPlayer("mia")name = "mia"score = 0Player("leo")name = "leo"score = 0
One class, many instances. Each instance carries its own attribute values, created by __init__.

Two instances, two separate scores

mia and leo are stamped from the same blueprint but each keeps its own data. Changing one never touches the other.

class Player:
    def __init__(self, name):
        self.name = name
        self.score = 0

mia = Player("mia")
leo = Player("leo")

mia.score = 50
print(mia.name, mia.score)
print(leo.name, leo.score)

Output

mia 50
leo 0

Each Player(...) call runs __init__ again on a brand-new object, so self.score = 0 creates a separate score attribute per instance. This per-instance isolation is the whole point of a class, and it is exactly what a single shared dict would not give you.

Storing parameters as attributes

A Book class whose __init__ takes title and pages and stores both on the instance, then two books created from it.

This is the plainest possible __init__: every parameter is copied straight onto self. Each Book(...) call produces a separate object with its own title and pages.

class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages

dune = Book("Dune", 412)
hamlet = Book("Hamlet", 160)
print(dune.title, dune.pages)
print(hamlet.title, hamlet.pages)

Output

Dune 412
Hamlet 160

Three things this code depends on

  • self is always the first parameter of __init__, and Python fills it in for you. Book("Dune", 412) passes two arguments, not three.
  • self.title = title is what makes the value outlive the call. A bare title = title would only touch the local parameter and vanish when __init__ returns.
  • Attributes are read back with dot notation, dune.title, and dune and hamlet never share state.

Computing an attribute at creation time

__init__ can compute attributes, not only copy parameters. This Circle stores radius and also derives self.diameter as radius * 2 the moment the object is built.

Any expression can appear on the right-hand side, so anything cheap and always-needed can be settled once here rather than recalculated at every use.

class Circle:
    def __init__(self, radius):
        self.radius = radius
        self.diameter = radius * 2

c = Circle(5)
print(c.radius, c.diameter)

Output

5 10

What to take away

  • Any expression can go on the right inside __init__, as in self.diameter = radius * 2.
  • Both attributes live on the instance afterwards, so c.radius is 5 and c.diameter is 10.
  • There is a trade-off. diameter is computed once, so reassigning c.radius later leaves c.diameter stale. For values that must always agree, a computed property (unit 4) is the better tool.

When __init__ runs

In p = Player("mia"), __init__ runs automatically, as part of creating the new object.

Calling the class like a function creates a fresh instance, and Python immediately hands that instance to __init__ as self along with the rest of the arguments. You almost never call __init__ by hand.

So it is not something you invoke yourself with p.__init__(), it does not run once at class-definition time (that is when the class body executes, which is a different moment), and it has nothing to do with program shutdown. The method that runs at teardown is __del__, which is rarely used.