Unit 3 · Classes, Modules & Files

Lesson · Unit 3 · 9 min read

__init__ and attributes, how instances get their state.

Every time you create an instance, Python runs __init__ to set up its starting state. Here's how that works, the difference between instance and class attributes, and the trap that puts shared data where you didn't expect it.

Section · 01

__init__ runs once per instance

__init__is a special method (those double underscores mean “Python calls this for you”) that runs automatically whenever you create an instance. Its job is to set up the instance’s starting state.

class BankAccount:
    def __init__(self, owner, opening_balance=0):
        self.owner = owner
        self.balance = opening_balance

# Calling the class triggers __init__
acct = BankAccount("Ada", 100)

print(acct.owner)        # "Ada"
print(acct.balance)      # 100

When you write BankAccount("Ada", 100):

1. Python creates a new, empty BankAccount object.
2. It calls __init__ on that object, passing it in as self.
3. __init__ assigns attributes onto self.
4. The fully-constructed object is what BankAccount(...) returns.

You never call __init__ directly. You call the class — BankAccount(...) — and Python wires up the rest.

Section · 02

self is the instance

The first parameter of __init__ (and every regular method) is self. It’s a reference to the specific instance the method is being called on. Inside the method, self.something = value attaches an attribute to this instance.

class BankAccount:
    def __init__(self, owner, opening_balance=0):
        self.owner = owner             # attach owner to this instance
        self.balance = opening_balance # attach balance to this instance

a = BankAccount("Ada", 100)
b = BankAccount("Ben", 250)

a.owner          # "Ada"
b.owner          # "Ben"

a.balance = 999  # only changes a, not b
b.balance        # still 250

The name self is just convention. Python passes the instance in as the first argument no matter what you call it. Everyone uses self. Use self.

Section · 03

Instance attributes vs class attributes

You can also put attributes directly on the class — outside any method. Those are class attributes. They’re shared across every instance.

class BankAccount:
    # Class attribute — shared
    interest_rate = 0.045

    def __init__(self, owner, opening_balance=0):
        # Instance attributes — per-object
        self.owner = owner
        self.balance = opening_balance

a = BankAccount("Ada", 100)
b = BankAccount("Ben", 250)

a.interest_rate          # 0.045 (looked up on the class)
b.interest_rate          # 0.045 (same one)

# Change it on the CLASS — affects all instances
BankAccount.interest_rate = 0.05
a.interest_rate          # 0.05
b.interest_rate          # 0.05

When to use which:

Instance attribute — different for each instance (owner, balance)
Class attribute     — same for all instances (a tax rate, a config value,
                      a default constant)

A common gotcha: assigning to a class attribute via an instance actually creates an instance attribute that shadows the class one. This trips up people who think they’re updating a shared value:

a.interest_rate = 0.10        # creates an INSTANCE attribute on a
a.interest_rate               # 0.10
b.interest_rate               # 0.05 — unchanged, still using the class one
BankAccount.interest_rate     # 0.05 — class attribute also unchanged

Section · 04

The mutable class-attribute trap

We hit a version of this in Unit 1 (mutable default arguments). The class-attribute version is sneakier:

class Inbox:
    messages = []        # WRONG — shared across all instances!

    def add(self, msg):
        self.messages.append(msg)

a = Inbox()
b = Inbox()

a.add("hello")
print(b.messages)         # ["hello"]   — surprise!

Both a and b point at the same list, because messages lives on the class, not the instance. Fix: initialize the mutable in __init__ so each instance gets its own:

class Inbox:
    def __init__(self):
        self.messages = []      # fresh list per instance

a = Inbox()
b = Inbox()

a.messages.append("hello")
b.messages         # []

Rule of thumb: class attributes are fine for immutable defaults (numbers, strings, tuples, booleans). For lists, dicts, sets, and any custom object — initialize in __init__.

Section · 05

Adding attributes from outside

Python doesn’t lock down what attributes an instance can have. You can add new ones from outside the class:

acct = BankAccount("Ada", 100)
acct.nickname = "Main checking"     # totally legal — new attribute

print(acct.nickname)

This is technically allowed, but it’s a smell. Ifnickname is a meaningful piece of an account, it should be in __init__ alongside owner and balance. Otherwise you have accounts in two shapes (some with nickname, some without), and every downstream function has to check.

The discipline: declare every attribute in __init__ — even if you set it to None initially. That way, a reader who lands in your class has one place to look to know what each instance carries:

class BankAccount:
    def __init__(self, owner, opening_balance=0):
        self.owner = owner
        self.balance = opening_balance
        self.nickname = None           # explicit "not set yet"
        self.last_login = None

Future you in six months will thank you. Future them who has to debug it definitely will.

Curriculum source

Lesson content is original to YorkSims. Topic structure aligns with Python for Everybody by Dr. Charles R. Severance (py4e.com), licensed under Creative Commons Attribution 3.0 Unported.