Classes & OOP
Object-Oriented Programming lets you model the world as objects with state and behaviour. Master classes and you unlock the design patterns behind every major Python library.
What is a Class?
A class is a blueprint. An object (instance) is something built from that blueprint. You can make as many objects from one class as you want, each with its own data.
class Dog:
# __init__ is the constructor — runs when you create an instance
def __init__(self, name: str, breed: str, age: int):
self.name = name # instance attributes — unique per object
self.breed = breed
self.age = age
def bark(self): # instance method
return f"{self.name} says: Woof!"
def info(self):
return f"{self.name} ({self.breed}), {self.age} yr old"
def __repr__(self): # what prints when you inspect the object
return f"Dog(name={self.name!r}, breed={self.breed!r})"
# Create instances
rex = Dog("Rex", "Labrador", 3)
luna = Dog("Luna", "Poodle", 5)
print(rex.bark()) # Rex says: Woof!
print(luna.info()) # Luna (Poodle), 5 yr old
print(rex) # Dog(name='Rex', breed='Labrador')
print(rex.name) # Rex
Output
What is
self?self is a reference to the current instance. When you call rex.bark(), Python silently passes rex as the first argument — that's self.Class Attributes and Class Methods
class BankAccount:
interest_rate = 0.03 # class attribute — shared by all instances
def __init__(self, owner: str, balance: float = 0):
self.owner = owner
self.balance = balance
def deposit(self, amount: float):
self.balance += amount
return self.balance
def withdraw(self, amount: float):
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
return self.balance
def apply_interest(self):
self.balance *= (1 + BankAccount.interest_rate)
@classmethod
def from_dict(cls, data: dict):
"""Alternative constructor"""
return cls(data["owner"], data.get("balance", 0))
@staticmethod
def is_valid_amount(amount):
return isinstance(amount, (int, float)) and amount > 0
acc = BankAccount("Alice", 1000)
acc.deposit(500)
acc.apply_interest()
print(f"Balance: ${acc.balance:.2f}") # 1545.00
acc2 = BankAccount.from_dict({"owner": "Bob", "balance": 200})
print(acc2.owner, acc2.balance)
print(BankAccount.is_valid_amount(-5)) # False
Output
Inheritance
A child class inherits all attributes and methods from its parent. It can override them or add new ones.
class Animal:
def __init__(self, name: str):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
def __str__(self):
return f"{type(self).__name__}({self.name!r})"
class Dog(Animal):
def speak(self): # override
return f"{self.name}: Woof!"
def fetch(self, item: str): # new method
return f"{self.name} fetches the {item}!"
class Cat(Animal):
def speak(self):
return f"{self.name}: Meow!"
def purr(self):
return f"{self.name} purrs..."
animals = [Dog("Rex"), Cat("Whiskers"), Dog("Buddy")]
for animal in animals:
print(animal.speak()) # polymorphism!
print(isinstance(animals[0], Dog)) # True
print(isinstance(animals[0], Animal)) # True — Dog IS-A Animal
Output
Dunder Methods (Magic Methods)
Methods with double underscores like __len__, __add__, __eq__ let your objects behave like built-in types.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __add__(self, other): # v1 + v2
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar): # v * 3
return Vector(self.x * scalar, self.y * scalar)
def __eq__(self, other): # v1 == v2
return self.x == other.x and self.y == other.y
def __len__(self): # len(v)
return 2 # a 2D vector has 2 components
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v1 * 3) # Vector(3, 6)
print(v1 == Vector(1, 2)) # True
print(len(v1)) # 2
Output