Lesson 128 min read

Classes & OOP

Build your own data types — with superpowers

What Are Classes?

A class is like a cookie cutter. The cookie cutter itself isn't a cookie — it's a template for making cookies. Each cookie you stamp out is an object (also called an instance). Every cookie has the same shape, but you can decorate them differently (different attributes).

In programming terms:

  • Class = the blueprint (defines what data and behaviors something has)
  • Object/Instance = a specific thing made from that blueprint
  • Attributes = data stored on the object (like name, age), often using types like lists and dictionaries
  • Methods = functions that belong to the object (like speak(), jump())

Your First Class

class Dog:
# Class attribute (shared by ALL dogs)
species = "Canis familiaris"
def __init__(self, name, breed, age):
# Instance attributes (unique to each dog)
self.name = name
self.breed = breed
self.age = age
self.tricks = [] # Each dog starts with no tricks
def bark(self):
return f"{self.name} says: Woof!"
def learn_trick(self, trick):
self.tricks.append(trick)
return f"{self.name} learned {trick}!"
# Create instances (objects)
buddy = Dog("Buddy", "Golden Retriever", 3)
max_dog = Dog("Max", "German Shepherd", 5)
print(buddy.bark())
print(max_dog.bark())
print(buddy.learn_trick("shake"))
print(buddy.learn_trick("roll over"))
print(f"{buddy.name}'s tricks: {buddy.tricks}")
print(f"{max_dog.name}'s tricks: {max_dog.tricks}") # Still empty!
Output
Buddy says: Woof!
Max says: Woof!
Buddy learned shake!
Buddy learned roll over!
Buddy's tricks: ['shake', 'roll over']
Max's tricks: []

The self Parameter

Every method in a class takes self as its first parameter. Think of self as a mirror — it lets the object refer to itself. When you call buddy.bark(), Python secretly passes buddy as self, so the method knows which dog is barking.

The __init__ method is the constructor — it runs automatically when you create a new object. It's where you set up the object's starting state.

Special (Dunder) Methods

Python has special methods surrounded by double underscores ("dunder" methods) that let your objects work with built-in operations like print(), len(), +, and comparisons.

  • __str__ — What print() and str() use. Make your object human-readable.
  • __repr__ — The "developer" view. Should ideally show how to recreate the object.
  • __len__ — Makes len(obj) work.
  • __eq__ — Defines what == means for your objects.

Dunder Methods in Action

class ShoppingCart:
def __init__(self):
self.items = []
def add(self, item, price):
self.items.append({"item": item, "price": price})
@property
def total(self):
return sum(i["price"] for i in self.items)
def __str__(self):
lines = [f" {i['item']}: ${i['price']:.2f}" for i in self.items]
return "Shopping Cart:\n" + "\n".join(lines) + f"\n Total: ${self.total:.2f}"
def __len__(self):
return len(self.items)
def __contains__(self, item_name):
return any(i["item"] == item_name for i in self.items)
cart = ShoppingCart()
cart.add("Coffee", 4.50)
cart.add("Muffin", 3.25)
cart.add("Juice", 5.00)
print(cart) # Uses __str__
print(f"Items: {len(cart)}") # Uses __len__
print(f"Has Coffee: {'Coffee' in cart}") # Uses __contains__
Output
Shopping Cart:
  Coffee: $4.50
  Muffin: $3.25
  Juice: $5.00
  Total: $12.75
Items: 3
Has Coffee: True

Inheritance — Building on What Exists

Inheritance lets you create a new class based on an existing one. The new class (child) gets all the attributes and methods of the existing class (parent), and can add or override them.

Think of it like a family business: the child inherits the parent's recipes (methods) and ingredients (attributes), but can also develop their own specialties.

Inheritance & super()

class Animal:
def __init__(self, name, sound):
self.name = name
self.sound = sound
def speak(self):
return f"{self.name} says {self.sound}!"
def __str__(self):
return f"{self.name} the {self.__class__.__name__}"
class Cat(Animal):
def __init__(self, name, indoor=True):
super().__init__(name, "Meow") # Call parent's __init__
self.indoor = indoor
def purr(self): # New method, only for cats
return f"{self.name} purrs softly..."
class Parrot(Animal):
def __init__(self, name, vocabulary=None):
super().__init__(name, "Squawk")
self.vocabulary = vocabulary or []
def speak(self): # Override parent method
if self.vocabulary:
import random
word = random.choice(self.vocabulary)
return f"{self.name} says: '{word}'"
return super().speak() # Fall back to parent
# Create instances
whiskers = Cat("Whiskers")
polly = Parrot("Polly", ["Hello!", "Pretty bird!", "Want a cracker?"])
print(whiskers.speak()) # Inherited from Animal
print(whiskers.purr()) # Cat-only method
print(polly.speak()) # Overridden method
print()
# isinstance checks
print(f"Is Whiskers an Animal? {isinstance(whiskers, Animal)}")
print(f"Is Polly a Cat? {isinstance(polly, Cat)}")
Output
Whiskers says Meow!
Whiskers purrs softly...
Polly says: 'Hello!'

Is Whiskers an Animal? True
Is Polly a Cat? False
Note: A common beginner mistake: forgetting to call super().__init__() in the child class. Without it, the parent's setup code never runs, and your object will be missing attributes. Always call super() first in your child's __init__!

Quick check

What does self refer to in a method?
Error HandlingModules & Imports