Classes and Instances

Updated

September 8, 2026

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

Output:

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

Output:

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

Output:

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

Output:

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

Output:

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 on self. 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

  1. In ticket.py, add a method move(self, table) that sets self.table. Print label before and after a move.
  2. In ticket_fn.py, add a function move_ticket(ticket, table) and a method move. Show both.
  3. In shift_tickets.py, add Shift.tables that returns a list of table numbers in ticket order.
  4. In shared_notes.py, move notes = [] into __init__ as self.notes = [] and confirm b.notes stays empty.