Classes & Objects
A class is a blueprint for objects that bundle data (attributes) with the functions that work on it (methods). Everything in Python is already an object — strings, lists, even functions — and classes let you create your own types that fit your problem.
Defining a class
class Dog: species = "Canis familiaris" def __init__(self, name, age): self.name = name self.age = age def describe(self): return f"{self.name} is {self.age} years old" def birthday(self): self.age += 1 rex = Dog("Rex", 3) fido = Dog("Fido", 7) print(rex.describe()) rex.birthday() print(rex.age, fido.age) print(rex.species, fido.species)
Rex is 3 years old 4 7 Canis familiaris Canis familiaris
class Dog:defines the type. Class names use PascalCase.__init__is the initialiser, run automatically when you create an object withDog(...). It sets up the object's attributes.selfis the object the method was called on. Python passes it automatically:rex.describe()callsDog.describe(rex).self.name = namecreates an instance attribute — each object has its own.speciesis a class attribute, shared by every instance.
Objects are independent
class Counter: def __init__(self): self.count = 0 def increment(self, by=1): self.count += by return self a = Counter() b = Counter() a.increment().increment(5) print(a.count, b.count) print(isinstance(a, Counter), type(a).__name__)
6 0 True Counter
Returning self from a method allows calls to be chained.
Class attribute pitfall
A mutable class attribute is shared by all instances — usually not what you want:
class BadCart: items = [] def add(self, item): self.items.append(item) class Cart: def __init__(self): self.items = [] def add(self, item): self.items.append(item) a, b = BadCart(), BadCart() a.add("apple") print(b.items) c, d = Cart(), Cart() c.add("apple") print(d.items)
['apple'] []
Create per-object data in __init__. Use class attributes only for constants and genuinely shared values.
Readable printing: __repr__ and __str__
class Point: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f"Point(x={self.x}, y={self.y})" def __str__(self): return f"({self.x}, {self.y})" p = Point(3, 4) print(p) print(repr(p)) print([p, Point(0, 0)])
(3, 4) Point(x=3, y=4) [Point(x=3, y=4), Point(x=0, y=0)]
Without these methods, printing shows something like <__main__.Point object at 0x…>. __repr__ is for developers (used in lists, the shell and debuggers); __str__ is for users. Lesson 38 covers more of these "special methods".
Attributes are dynamic
class Box: pass b = Box() b.width = 10 b.label = "books" print(vars(b)) print(getattr(b, "height", "no height")) setattr(b, "height", 5) print(hasattr(b, "height"))
{'width': 10, 'label': 'books'}
no height
TrueYou can add attributes to most objects at any time, and getattr/setattr/hasattr access them by name. It's flexible, but defining all attributes in __init__ keeps classes predictable.
Private by convention
Python has no truly private attributes. A leading underscore signals "internal, don't touch from outside":
class Account: def __init__(self, owner, balance=0): self.owner = owner self._balance = balance def deposit(self, amount): if amount <= 0: raise ValueError("Deposit must be positive") self._balance += amount def balance(self): return self._balance acc = Account("Ana", 100) acc.deposit(50) print(acc.owner, acc.balance())
Ana 150
A double underscore (self.__balance) triggers name mangling, making accidental access from subclasses harder — but it's still not real privacy. Python relies on convention: "we're all consenting adults".
When to write a class
Classes are worth it when data and the operations on it belong together and you'll create several instances — a BankAccount, a Player, an APIClient. For a bundle of data with no behaviour, a dataclass or dictionary is simpler; for a single function, just write the function.