Classes and Instances
Classes and Instances
A class is a blueprint. An instance is one concrete object built from it. The boring default is a small class with __init__ to store data, and methods that use that data. If you have no state that lives, write a function.
Mental model
class Ticket: creates a class object. Ticket(7, 12) calls __init__ and returns an instance. self is that instance. Every method’s first parameter is self. You do not pass it at the call site: t.label() passes t for you.
A method is a function defined on a class. On the class it is a function. On the instance it is a bound method: self is already filled in. A plain function that takes a ticket is still valid. Use a method when the behaviour belongs with the data and you will call it on many instances.
Instance attributes live on self (self.id). A name assigned on the class body is shared by all instances until an instance overrides it.
Worked examples
Case 1: class, __init__, self
Save as ticket.py. __init__ stores two numbers. It returns None on purpose; the instance is created by the class machinery.
# ticket.py
class Ticket:
def __init__(self, id, table):
self.id = id
self.table = table
def label(self):
return f"ticket {self.id} → table {self.table}"
def main():
t = Ticket(7, 12)
print(t.label())
print(t.id, t.table)
if __name__ == "__main__":
main()Run:
uv run python ticket.pyOutput:
ticket 7 → table 12
7 12
Two instances are two objects. Changing one table does not change the other.
Case 2: Methods versus functions
Save as ticket_fn.py. The same label is a method and a function. Both are correct. The method travels with the class. The function works on anything with .id and .table.
# ticket_fn.py
class Ticket:
def __init__(self, id, table):
self.id = id
self.table = table
def label(self):
return f"ticket {self.id} → table {self.table}"
def label_ticket(ticket):
return f"ticket {ticket.id} → table {ticket.table}"
def main():
t = Ticket(7, 12)
print(t.label())
print(label_ticket(t))
print(Ticket.label(t))
print(type(t.label).__name__)
print(type(Ticket.label).__name__)
if __name__ == "__main__":
main()Run:
uv run python ticket_fn.pyOutput:
ticket 7 → table 12
ticket 7 → table 12
ticket 7 → table 12
method
function
t.label() is the everyday call. Ticket.label(t) is the same function with self passed by hand. Prefer t.label() in application code.
Case 3: Several instances
Save as shift_tickets.py. A shift holds a list of tickets. The desk object does not need a hierarchy; it needs a list.
# shift_tickets.py
class Ticket:
def __init__(self, id, table):
self.id = id
self.table = table
def label(self):
return f"ticket {self.id} → table {self.table}"
class Shift:
def __init__(self, name):
self.name = name
self.tickets = []
def add(self, ticket):
self.tickets.append(ticket)
def dump(self):
print(self.name)
for t in self.tickets:
print(" ", t.label())
def main():
morning = Shift("morning")
morning.add(Ticket(1, 4))
morning.add(Ticket(2, 12))
morning.dump()
print("count", len(morning.tickets))
if __name__ == "__main__":
main()Run:
uv run python shift_tickets.pyOutput:
morning
ticket 1 → table 4
ticket 2 → table 12
count 2
Shift has state (tickets) and behaviour that belongs to it (add, dump). That is enough reason for a class.
The trap
A mutable assigned in the class body is one object, shared by every instance. Two tickets, one notes list.
Save as shared_notes.py:
# shared_notes.py
class Ticket:
notes = []
def __init__(self, id):
self.id = id
def add_note(self, text):
self.notes.append(text)
def main():
a = Ticket(1)
b = Ticket(2)
a.add_note("window")
print("a", a.notes)
print("b", b.notes)
print("same", a.notes is b.notes)
if __name__ == "__main__":
main()Run:
uv run python shared_notes.pyOutput:
a ['window']
b ['window']
same True
The fix is an instance list in __init__:
# own_notes.py
class Ticket:
def __init__(self, id):
self.id = id
self.notes = []
def add_note(self, text):
self.notes.append(text)
def main():
a = Ticket(1)
b = Ticket(2)
a.add_note("window")
print("a", a.notes)
print("b", b.notes)
print("same", a.notes is b.notes)
if __name__ == "__main__":
main()Run:
uv run python own_notes.pyOutput:
a ['window']
b []
same False
Class attributes are fine for constants (kind = "ticket"). They are not fine for lists you append to.
The boring rule
- Class = data that lives + behaviour that belongs to it. Otherwise write a function.
__init__stores attributes onself. It does not return the instance.- Call methods on the instance:
t.label(). - Put mutable per-instance state in
__init__, not on the class body. - Two instances are two objects. Do not reuse one ticket as a “template” by mutating it in place unless that is the real lifecycle.
Try this
- In
ticket.py, add a methodmove(self, table)that setsself.table. Printlabelbefore and after a move. - In
ticket_fn.py, add a functionmove_ticket(ticket, table)and a methodmove. Show both. - In
shift_tickets.py, addShift.tablesthat returns a list of table numbers in ticket order. - In
shared_notes.py, movenotes = []into__init__asself.notes = []and confirmb.notesstays empty.