Week 5 Code Examples

Object-Oriented Programming — Classes, Inheritance & Polymorphism

Object-Oriented Programming (OOP) is the dominant way large programs are organized. Instead of a collection of loose functions and variables, you group related data and behavior together into objects. This week you'll learn to think in objects.

How to use this page: Copy any example into a .py file and run it. OOP concepts build on each other — read the sections in order.

1. Classes and Objects — Blueprints and Instances

A class is a blueprint that describes what data an object holds and what it can do. An object (or instance) is a specific thing created from that blueprint.

Key concept: A class is like a cookie cutter. An object is the cookie. You can make many different cookies from the same cutter — they all have the same shape (structure), but different frosting (data).

Defining a class

class Dog: pass # empty class — valid Python, does nothing yet fido = Dog() # create an instance buddy = Dog() # a different instance print(type(fido)) # print(fido is buddy) # False — they are two separate objects

2. __init__ and Instance Attributes

__init__ is a special method that runs automatically when an object is created. It's where you set up the object's initial data. self refers to the specific instance being created.

class Dog: def __init__(self, name, breed, age): self.name = name # instance attribute — unique to this object self.breed = breed self.age = age def describe(self): print(f"{self.name} is a {self.age}-year-old {self.breed}.") def bark(self): print(f"{self.name} says: Woof!") # Create two independent Dog objects fido = Dog("Fido", "Labrador", 3) buddy = Dog("Buddy", "Poodle", 7) fido.describe() # Fido is a 3-year-old Labrador. buddy.describe() # Buddy is a 7-year-old Poodle. fido.bark() # Fido says: Woof! # Attributes are unique per instance print(fido.name) # Fido print(buddy.name) # Buddy fido.age = 4 # only changes fido, not buddy print(buddy.age) # still 7
self is required: Every method in a class must have self as its first parameter. Python passes the instance automatically — you never pass it yourself when calling the method. Forgetting self is a very common beginner mistake.

3. Encapsulation — Protecting Data

Encapsulation means keeping an object's internal data hidden and only accessible through its methods. In Python, prefix attribute names with an underscore (_balance) to signal "don't access this directly."

Key concept: When outside code can only change data through methods, the object can validate and control every change. There's no way to accidentally set a balance to a negative number if only the deposit() method can modify it.
class Thermostat: def __init__(self, initial_temp): self._temp = initial_temp # underscore = private by convention def get_temp(self): return self._temp def set_temp(self, new_temp): if new_temp < 50 or new_temp > 90: raise ValueError(f"Temperature {new_temp} is out of safe range (50–90°F)") self._temp = new_temp print(f"Temperature set to {self._temp}°F") def __str__(self): return f"Thermostat({self._temp}°F)" t = Thermostat(72) t.set_temp(68) # Temperature set to 68°F print(t.get_temp()) # 68 try: t.set_temp(200) # raises ValueError except ValueError as e: print(f"Error: {e}") # Error: Temperature 200 is out of safe range print(t) # Thermostat(68°F) — __str__ controls how it prints

4. Inheritance — Building on Existing Classes

Inheritance lets a new class (child) automatically get all the attributes and methods of an existing class (parent), then add or change what it needs. This eliminates rewriting shared behavior.

Key concept: Inheritance models "is-a" relationships. An ElectricCar is a Vehicle. A SavingsAccount is an Account. The child gets everything the parent has, plus its own extras.

Basic inheritance

class Vehicle: def __init__(self, make, model, year): self._make = make self._model = model self._year = year def describe(self): return f"{self._year} {self._make} {self._model}" def start(self): print(f"{self.describe()} is starting...") class ElectricCar(Vehicle): # ElectricCar inherits from Vehicle def __init__(self, make, model, year, range_miles): super().__init__(make, model, year) # run Vehicle's __init__ first self._range = range_miles # add the extra attribute def charge_status(self): print(f"Range: {self._range} miles per charge") # ElectricCar has BOTH Vehicle's methods AND its own tesla = ElectricCar("Tesla", "Model 3", 2025, 358) tesla.start() # 2025 Tesla Model 3 is starting... (from Vehicle) tesla.charge_status() # Range: 358 miles per charge (from ElectricCar) print(tesla.describe()) # 2025 Tesla Model 3 (from Vehicle)

super() — calling the parent's method

super() gives you access to the parent class so you can extend its behavior instead of completely replacing it.

class Animal: def __init__(self, name): self._name = name def speak(self): return f"{self._name} makes a sound" class Cat(Animal): def speak(self): # Call the parent version, then add to it base = super().speak() return base + " — specifically: Meow!" class Dog(Animal): def speak(self): return f"{self._name} says: Woof!" # completely override, no super() cat = Cat("Whiskers") dog = Dog("Rex") print(cat.speak()) # Whiskers makes a sound — specifically: Meow! print(dog.speak()) # Rex says: Woof!

5. Polymorphism — Same Method, Different Behavior

Polymorphism means you can call the same method on different object types and each one responds in its own way. This is powerful because the calling code doesn't need to know what type of object it has.

Key concept: Polymorphism lets you write one loop that works with any subclass — now and in the future. Adding a new subclass requires zero changes to the existing loop.
class Shape: def area(self): raise NotImplementedError("Every shape must implement area()") def describe(self): print(f"{self.__class__.__name__}: area = {self.area():.2f}") class Circle(Shape): def __init__(self, radius): self._radius = radius def area(self): return 3.14159 * self._radius ** 2 class Rectangle(Shape): def __init__(self, width, height): self._width = width self._height = height def area(self): return self._width * self._height class Triangle(Shape): def __init__(self, base, height): self._base = base self._height = height def area(self): return 0.5 * self._base * self._height # One loop works for ALL shapes — this is polymorphism shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)] for shape in shapes: shape.describe() # Output: # Circle: area = 78.54 # Rectangle: area = 24.00 # Triangle: area = 12.00

6. isinstance() — Checking Object Types

isinstance(obj, ClassName) returns True if obj is an instance of that class (or any of its subclasses). This lets you branch behavior based on what type of object you're working with.

class Animal: def __init__(self, name): self._name = name @property def name(self): return self._name def __str__(self): return self._name class Dog(Animal): def fetch(self): print(f"{self._name} fetches the ball!") class Cat(Animal): def purr(self): print(f"{self._name} purrs...") pets = [Dog("Rex"), Cat("Whiskers"), Dog("Buddy"), Cat("Luna")] for pet in pets: print(f"{pet.name} is a Dog: {isinstance(pet, Dog)}") # use the property if isinstance(pet, Dog): pet.fetch() elif isinstance(pet, Cat): pet.purr() # isinstance also works with parent classes: print(isinstance(pets[0], Animal)) # True — Dog IS an Animal

7. __str__ — Controlling How Objects Print

__str__ is the method Python calls automatically when you print() an object or convert it to a string with str(). Without it, you get an unhelpful memory address.

class Player: def __init__(self, name, level, score): self._name = name self._level = level self._score = score def __str__(self): return f"Player('{self._name}' | Level {self._level} | Score: {self._score:,})" p = Player("Alice", 12, 48750) print(p) # Player('Alice' | Level 12 | Score: 48,750) print(str(p)) # same thing players = [Player("Alice", 12, 48750), Player("Bob", 7, 12300)] for player in players: print(player) # __str__ is called automatically each time

8. Putting It All Together — A Library System

A small library catalog that uses a base class, two subclasses, encapsulation, polymorphism, and a simple manager class — all with no project overlap.

class LibraryItem: def __init__(self, title, item_id): self._title = title self._item_id = item_id self._checked_out = False def checkout(self): if self._checked_out: print(f"'{self._title}' is already checked out.") else: self._checked_out = True print(f"Checked out: '{self._title}'") def return_item(self): self._checked_out = False print(f"Returned: '{self._title}'") def is_available(self): return not self._checked_out @property def item_id(self): return self._item_id def summary(self): raise NotImplementedError class Book(LibraryItem): def __init__(self, title, item_id, author, pages): super().__init__(title, item_id) self._author = author self._pages = pages def summary(self): status = "available" if self.is_available() else "checked out" return f"[Book] '{self._title}' by {self._author} ({self._pages} pages) — {status}" class DVD(LibraryItem): def __init__(self, title, item_id, runtime_mins): super().__init__(title, item_id) self._runtime = runtime_mins def summary(self): status = "available" if self.is_available() else "checked out" return f"[DVD] '{self._title}' ({self._runtime} min) — {status}" class Library: def __init__(self): self._items = {} def add_item(self, item): self._items[item.item_id] = item def list_all(self): for item in self._items.values(): print(item.summary()) def find(self, item_id): return self._items.get(item_id) # --- Run it --- lib = Library() lib.add_item(Book("The Martian", "B001", "Andy Weir", 369)) lib.add_item(Book("Clean Code", "B002", "Robert Martin", 431)) lib.add_item(DVD("2001: A Space Odyssey", "D001", 149)) lib.list_all() # [Book] 'The Martian' by Andy Weir (369 pages) — available # [Book] 'Clean Code' by Robert Martin (431 pages) — available # [DVD] '2001: A Space Odyssey' (149 min) — available lib.find("B001").checkout() # Checked out: 'The Martian' lib.find("B001").checkout() # 'The Martian' is already checked out. print() lib.list_all() # [Book] 'The Martian' by Andy Weir (369 pages) — checked out # ... lib.find("B001").return_item() # Returned: 'The Martian'
Notice: Library.list_all() calls summary() on every item — it doesn't know or care whether the item is a Book or DVD. That's polymorphism at work. Adding a Magazine class later requires zero changes to Library.