11 Classes in Python: The Absolute Minimum You Must Know
Everything about Python classes follows from one model — a class is a blueprint, an instance is one object built from it — plus about five pieces of mechanics, all on this page. The running example is a bank account, because it’s the smallest thing with both data and rules.
11.1 The Blueprint and the Built Object
class BankAccount:
bank_name = "First Doctest Savings" # class attribute: shared by ALL accounts
def __init__(self, owner, balance=0):
self.owner = owner # instance attributes: THIS account only
self.balance = balance
def deposit(self, amount):
self.balance += amount
return self.balance
def __repr__(self):
return f"BankAccount(owner={self.owner!r}, balance={self.balance})">>> acct = BankAccount("Amira", 100)
>>> other = BankAccount("Ben")
>>> acct.deposit(50)
150
>>> other.balance # untouched — separate instance, separate state
0Calling the class like a function (BankAccount("Amira", 100)) builds a fresh instance and runs __init__ on it. __init__ does not create the object — Python has already made an empty one — it initialises it, stamping on the attributes this instance starts with.
11.2 self Is Not Magic
self is just the instance, passed as the first argument automatically:
>>> acct = BankAccount("Amira", 100)
>>> acct.deposit(25) # the normal spelling...
125
>>> BankAccount.deposit(acct, 25) # ...is exactly this, spelled out
150That’s the whole trick. acct.deposit(25) means “run the blueprint’s deposit function on this object”. It also explains the classic error message deposit() takes 2 positional arguments but 3 were given — you forgot self in the def, so your own arguments shifted into its place.
11.3 Class Attributes vs Instance Attributes
bank_name lives on the class — one copy, visible through every instance. owner and balance live on each instance. Lookup goes instance first, then class:
>>> BankAccount.bank_name
'First Doctest Savings'
>>> BankAccount("Ben").bank_name # found on the class, via any instance
'First Doctest Savings'Rule of thumb: constants shared by every instance go on the class; everything that varies per object gets set on self in __init__. Never put a mutable value (a list, a dict) on the class “as a default” — every instance would share the same one (see the confabulation below).
11.4 __repr__: Make Your Objects Legible
Without __repr__, printing an object shows <__main__.BankAccount object at 0x...> — useless in debugging. With it:
>>> BankAccount("Amira", 200)
BankAccount(owner='Amira', balance=200)Convention: __repr__ returns the code you’d type to rebuild the object. Ten seconds of effort, repaid every time you look at a stack trace or a list of objects.
11.5 Dataclasses: The Modern Default for Data-Carrying Classes
When a class is mostly “hold these fields”, stop writing __init__ and __repr__ by hand — @dataclass generates them, plus == comparison, from the field list:
from dataclasses import dataclass
@dataclass
class Transaction:
amount: int
kind: str>>> t = Transaction(50, "deposit")
>>> t
Transaction(amount=50, kind='deposit')
>>> t == Transaction(50, "deposit") # plain classes would say False here
TrueReach for a dataclass first; fall back to a hand-written class when you need real behaviour and control, like BankAccount above. You can still add methods to a dataclass.
11.6 Directing the Machine
When you ask an AI for a class, name the parts from this page — which attributes are per-instance, what the repr should look like, whether a dataclass suffices — or you’ll get generic boilerplate with getters, setters, and inheritance you never asked for.
Vague:
"Write a bank account class."
Informed:
"Write a BankAccount class: instance attributes owner and balance (default 0), a
deposit method returning the new balance, and a __repr__ that rebuilds the object.
Also a Transaction dataclass with amount and kind. No getters/setters, no
inheritance."
11.7 Spot the Confabulation
An AI assistant adds transaction history to the account:
class LoggedAccount:
history = [] # keeps a record of every deposit
def __init__(self, owner):
self.owner = owner
def deposit(self, amount):
self.history.append(amount)What’s wrong?
history = [] is a class attribute, so every account shares one list. Watch:
>>> a, b = LoggedAccount("Amira"), LoggedAccount("Ben")
>>> a.deposit(100)
>>> b.history # Ben "has" Amira's deposit
[100]self.history.append(...) finds no history on the instance, falls through to the class, and mutates the shared list. Per-instance mutable state must be created in __init__: self.history = []. The AI version even looks more tidy — which is exactly why this is the most common class bug LLMs write.
11.8 Where to Practice
- Python Tutor — paste the
BankAccountcode and step through it: you’ll see the class object, each instance, and which box each attribute lives in. The fastest cure forselfconfusion. No signup. - W3Schools Python Classes exercises — small in-browser fill-in exercises on
__init__,self, and methods. Free, no signup.
11.9 Quick Reference
| Mechanic | What it does |
|---|---|
class Name: |
define a blueprint |
Name(args) |
build an instance; runs __init__ |
def __init__(self, ...) |
initialise a new instance’s attributes |
self |
the instance a method was called on (passed automatically) |
self.x = value |
instance attribute — this object only |
x = value at class level |
class attribute — shared by all instances |
def method(self, ...) |
behaviour; call as obj.method(...) |
def __repr__(self) |
debug string; return rebuild-the-object code |
@dataclass |
auto-generate __init__, __repr__, == from fields |
| mutable class attribute | bug: shared state across all instances |
That covers the absolute minimum! You can now define, instantiate, and debug classes and choose dataclasses when they fit — the rest of OOP (see TAMYMN-OOP.md) builds directly on these mechanics.