Lesson 3: Classes and Object-Oriented Programming#
Introduction#
Object-Oriented Programming (OOP) is like organizing your code the way the real world is organized. Think of classes as blueprints (like architectural plans for a house) and objects as the actual houses built from those blueprints.
Why OOP Matters:
Modularity: Break complex systems into manageable pieces
Reusability: Write code once, use it in many contexts
Maintainability: Changes in one place donโt break everything
Real-world modeling: Code that mirrors how we think about problems
What Youโll Learn:
Classes and objects (instances)
Instance variables and methods
Class variables and methods
Magic methods (dunder methods)
Properties and encapsulation
Inheritance and polymorphism
Composition over inheritance
Abstract base classes
Dataclasses (modern Python)
Common design patterns
1. Creating Your First Class#
A class is a blueprint for creating objects. Objects are instances of classes.
Basic Class Structure#
class Dog:
"""A simple Dog class."""
def __init__(self, name, age):
"""Initialize a new Dog instance.
Args:
name (str): The dog's name
age (int): The dog's age
"""
self.name = name # Instance variable
self.age = age # Instance variable
def bark(self):
"""Make the dog bark."""
return f"{self.name} says Woof!"
def get_info(self):
"""Return information about the dog."""
return f"{self.name} is {self.age} years old"
# Create objects (instances)
buddy = Dog("Buddy", 3)
max_dog = Dog("Max", 5)
print(buddy.bark()) # Buddy says Woof!
print(max_dog.get_info()) # Max is 5 years old
# Each object has its own data
print(f"Buddy's age: {buddy.age}")
print(f"Max's age: {max_dog.age}")
Understanding self#
self refers to the specific instance of the class. Itโs how the instance accesses its own data.
class Person:
def __init__(self, name):
self.name = name # self.name is the instance variable
def greet(self):
# self refers to the specific instance calling this method
print(f"Hello, I'm {self.name}")
alice = Person("Alice")
bob = Person("Bob")
alice.greet() # self = alice
bob.greet() # self = bob
2. Instance Variables vs Class Variables#
Instance Variables#
Unique to each instance (defined with self).
class Car:
def __init__(self, make, model):
self.make = make # Instance variable
self.model = model # Instance variable
car1 = Car("Toyota", "Camry")
car2 = Car("Honda", "Civic")
print(f"Car 1: {car1.make} {car1.model}")
print(f"Car 2: {car2.make} {car2.model}")
Class Variables#
Shared by all instances (defined in the class body).
class Dog:
# Class variable (shared by all dogs)
species = "Canis familiaris"
count = 0 # Track number of dogs created
def __init__(self, name):
self.name = name # Instance variable (unique to each dog)
Dog.count += 1 # Increment class variable
dog1 = Dog("Buddy")
dog2 = Dog("Max")
dog3 = Dog("Charlie")
# All dogs share the same species
print(f"{dog1.name}'s species: {dog1.species}")
print(f"{dog2.name}'s species: {dog2.species}")
# Class variable accessible from class
print(f"Total dogs created: {Dog.count}")
# Modifying class variable affects all instances
Dog.species = "Canis lupus familiaris"
print(f"Updated species for all: {dog1.species}")
3. Types of Methods#
Instance Methods#
Operate on instance data (use self).
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
# Instance method
def deposit(self, amount):
"""Deposit money into account."""
if amount > 0:
self.balance += amount
return f"Deposited ${amount}. Balance: ${self.balance}"
return "Invalid amount"
# Instance method
def withdraw(self, amount):
"""Withdraw money from account."""
if 0 < amount <= self.balance:
self.balance -= amount
return f"Withdrew ${amount}. Balance: ${self.balance}"
return "Insufficient funds"
account = BankAccount("Alice", 1000)
print(account.deposit(500))
print(account.withdraw(200))
Class Methods#
Operate on class data (use @classmethod decorator and cls).
class Pizza:
def __init__(self, ingredients):
self.ingredients = ingredients
def __repr__(self):
return f"Pizza({self.ingredients})"
@classmethod
def margherita(cls):
"""Factory method to create a Margherita pizza."""
return cls(['mozzarella', 'tomatoes', 'basil'])
@classmethod
def pepperoni(cls):
"""Factory method to create a Pepperoni pizza."""
return cls(['mozzarella', 'tomatoes', 'pepperoni'])
# Use class methods as factory methods
pizza1 = Pizza.margherita()
pizza2 = Pizza.pepperoni()
print(pizza1)
print(pizza2)
Static Methods#
Donโt use instance or class data (use @staticmethod decorator).
class Math:
@staticmethod
def add(x, y):
"""Add two numbers."""
return x + y
@staticmethod
def is_even(n):
"""Check if number is even."""
return n % 2 == 0
# Call static methods without creating an instance
print(Math.add(5, 3)) # 8
print(Math.is_even(10)) # True
# Can also call from instance (but uncommon)
m = Math()
print(m.add(10, 20)) # 30
4. Magic Methods (Dunder Methods)#
Magic methods (double underscore methods) allow you to define how objects behave with built-in operations.
__str__ and __repr__#
class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
def __str__(self):
"""Human-readable string (for end users)."""
return f"{self.title} by {self.author}"
def __repr__(self):
"""Developer-friendly representation (for debugging)."""
return f"Book('{self.title}', '{self.author}', {self.year})"
book = Book("1984", "George Orwell", 1949)
print(str(book)) # Uses __str__: 1984 by George Orwell
print(repr(book)) # Uses __repr__: Book('1984', 'George Orwell', 1949)
print(book) # print() uses __str__ if available
Comparison Magic Methods#
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
"""Check equality (==)."""
return self.age == other.age
def __lt__(self, other):
"""Less than (<)."""
return self.age < other.age
def __le__(self, other):
"""Less than or equal (<=)."""
return self.age <= other.age
def __repr__(self):
return f"Person('{self.name}', {self.age})"
alice = Person("Alice", 30)
bob = Person("Bob", 25)
charlie = Person("Charlie", 30)
print(f"alice == charlie: {alice == charlie}") # True (same age)
print(f"bob < alice: {bob < alice}") # True (25 < 30)
# Can now sort people by age
people = [alice, bob, charlie]
people.sort()
print(f"Sorted by age: {people}")
Arithmetic Magic Methods#
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""Add two vectors (+)."""
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
"""Multiply vector by scalar (*)."""
return Vector(self.x * scalar, self.y * scalar)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(2, 3)
v2 = Vector(5, 7)
print(f"v1 + v2 = {v1 + v2}") # Vector(7, 10)
print(f"v1 * 3 = {v1 * 3}") # Vector(6, 9)
Container Magic Methods#
class Playlist:
def __init__(self):
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __len__(self):
"""Return length with len()."""
return len(self.songs)
def __getitem__(self, index):
"""Enable indexing with []."""
return self.songs[index]
def __contains__(self, song):
"""Enable 'in' operator."""
return song in self.songs
playlist = Playlist()
playlist.add_song("Bohemian Rhapsody")
playlist.add_song("Stairway to Heaven")
playlist.add_song("Hotel California")
print(f"Playlist length: {len(playlist)}")
print(f"First song: {playlist[0]}")
print(f"Has Stairway: {'Stairway to Heaven' in playlist}")
# Can iterate because of __getitem__
for song in playlist:
print(f" - {song}")
5. Properties and Encapsulation#
Properties provide controlled access to attributes.
Using @property#
class Temperature:
def __init__(self, celsius):
self._celsius = celsius # "Private" attribute (convention)
@property
def celsius(self):
"""Get temperature in Celsius."""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Set temperature in Celsius with validation."""
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value
@property
def fahrenheit(self):
"""Get temperature in Fahrenheit."""
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Set temperature in Fahrenheit."""
self._celsius = (value - 32) * 5/9
temp = Temperature(25)
print(f"Celsius: {temp.celsius}ยฐC")
print(f"Fahrenheit: {temp.fahrenheit}ยฐF")
# Set using Fahrenheit
temp.fahrenheit = 100
print(f"New Celsius: {temp.celsius:.2f}ยฐC")
# Validation works
# temp.celsius = -300 # ValueError!
Computed Properties#
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def diameter(self):
"""Computed property."""
return self.radius * 2
@property
def area(self):
"""Computed property."""
import math
return math.pi * self.radius ** 2
@property
def circumference(self):
"""Computed property."""
import math
return 2 * math.pi * self.radius
circle = Circle(5)
print(f"Radius: {circle.radius}")
print(f"Diameter: {circle.diameter}")
print(f"Area: {circle.area:.2f}")
print(f"Circumference: {circle.circumference:.2f}")
6. Inheritance#
Inheritance allows classes to inherit attributes and methods from parent classes.
Basic Inheritance#
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species
def speak(self):
return "Some generic sound"
def get_info(self):
return f"{self.name} is a {self.species}"
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name, "Dog") # Call parent __init__
self.breed = breed
def speak(self): # Override parent method
return f"{self.name} says Woof!"
def fetch(self): # New method specific to Dog
return f"{self.name} fetches the ball!"
class Cat(Animal):
def __init__(self, name, color):
super().__init__(name, "Cat")
self.color = color
def speak(self):
return f"{self.name} says Meow!"
# Create instances
dog = Dog("Rex", "Golden Retriever")
cat = Cat("Whiskers", "Orange")
print(dog.get_info()) # Inherited method
print(dog.speak()) # Overridden method
print(dog.fetch()) # Dog-specific method
print(cat.get_info())
print(cat.speak())
Multiple Inheritance#
class Flyer:
def fly(self):
return "Flying through the air!"
class Swimmer:
def swim(self):
return "Swimming through water!"
class Duck(Flyer, Swimmer):
def __init__(self, name):
self.name = name
def quack(self):
return f"{self.name} says Quack!"
# Duck can both fly and swim
donald = Duck("Donald")
print(donald.fly())
print(donald.swim())
print(donald.quack())
7. Polymorphism#
Polymorphism allows objects of different classes to be treated uniformly.
class Shape:
def area(self):
raise NotImplementedError("Subclass must implement area()")
def perimeter(self):
raise NotImplementedError("Subclass must implement perimeter()")
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
def perimeter(self):
import math
return 2 * math.pi * self.radius
# Polymorphism in action
shapes = [
Rectangle(5, 3),
Circle(4),
Rectangle(10, 2)
]
for shape in shapes:
print(f"{shape.__class__.__name__}:")
print(f" Area: {shape.area():.2f}")
print(f" Perimeter: {shape.perimeter():.2f}")
8. Composition Over Inheritance#
Composition means building complex objects from simpler ones.
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
self.running = False
def start(self):
self.running = True
return "Engine started"
def stop(self):
self.running = False
return "Engine stopped"
class Wheel:
def __init__(self, size):
self.size = size
class Car:
"""Car HAS-A engine and wheels (composition)."""
def __init__(self, make, model, horsepower):
self.make = make
self.model = model
# Composition: Car contains other objects
self.engine = Engine(horsepower)
self.wheels = [Wheel(17) for _ in range(4)]
def start(self):
return self.engine.start()
def stop(self):
return self.engine.stop()
def get_info(self):
return f"{self.make} {self.model} with {self.engine.horsepower}hp"
car = Car("Toyota", "Camry", 203)
print(car.get_info())
print(car.start())
print(f"Engine running: {car.engine.running}")
9. Abstract Base Classes#
Abstract base classes define interfaces that subclasses must implement.
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
"""Abstract base class for payment processors."""
@abstractmethod
def process_payment(self, amount):
"""Process a payment. Must be implemented by subclasses."""
pass
@abstractmethod
def refund(self, amount):
"""Refund a payment. Must be implemented by subclasses."""
pass
class CreditCardProcessor(PaymentProcessor):
def process_payment(self, amount):
return f"Processing ${amount} via credit card"
def refund(self, amount):
return f"Refunding ${amount} to credit card"
class PayPalProcessor(PaymentProcessor):
def process_payment(self, amount):
return f"Processing ${amount} via PayPal"
def refund(self, amount):
return f"Refunding ${amount} via PayPal"
# Cannot instantiate abstract class
# processor = PaymentProcessor() # TypeError!
# Can instantiate concrete implementations
cc = CreditCardProcessor()
pp = PayPalProcessor()
print(cc.process_payment(100))
print(pp.process_payment(50))
10. Dataclasses (Python 3.7+)#
Dataclasses reduce boilerplate for classes that mainly store data.
from dataclasses import dataclass, field
from typing import List
@dataclass
class Product:
name: str
price: float
quantity: int = 0
tags: List[str] = field(default_factory=list)
def total_value(self):
return self.price * self.quantity
# Automatically generates __init__, __repr__, __eq__
p1 = Product("Laptop", 1200, 5, ["electronics", "computers"])
p2 = Product("Mouse", 25)
print(p1) # Product(name='Laptop', price=1200, quantity=5, ...)
print(f"Total value: ${p1.total_value()}")
# Equality comparison works
p3 = Product("Mouse", 25)
print(f"p2 == p3: {p2 == p3}") # True
Frozen Dataclasses (Immutable)#
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
p = Point(10, 20)
print(p)
# Cannot modify frozen dataclass
# p.x = 30 # FrozenInstanceError!
11. Design Patterns#
Singleton Pattern#
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
# All instances are the same object
s1 = Singleton()
s2 = Singleton()
print(f"s1 is s2: {s1 is s2}") # True
print(f"id(s1): {id(s1)}")
print(f"id(s2): {id(s2)}")
Factory Pattern#
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
class AnimalFactory:
@staticmethod
def create_animal(animal_type):
if animal_type == "dog":
return Dog()
elif animal_type == "cat":
return Cat()
else:
raise ValueError(f"Unknown animal type: {animal_type}")
# Use factory to create objects
dog = AnimalFactory.create_animal("dog")
cat = AnimalFactory.create_animal("cat")
print(dog.speak())
print(cat.speak())
Exercises#
Exercise 1: Basic Class#
Create a Rectangle class with:
widthandheightattributesarea()andperimeter()methodsis_square()method__str__()method that returns โRectangle(width x height)โ
# Your code here
Exercise 2: Properties#
Create a Person class with:
Private
_ageattributeageproperty with getter and setterSetter should validate age is between 0 and 150
birth_yearproperty that calculates from age (current year - age)
from datetime import datetime
# Your code here
Exercise 3: Inheritance#
Create a class hierarchy:
Vehiclebase class withbrand,year, andstart_engine()methodCarsubclass that addsnum_doorsMotorcyclesubclass that addshas_sidecarOverride
start_engine()in each subclass with specific messages
# Your code here
Exercise 4: Magic Methods#
Create a Money class that:
Stores amount and currency
Implements
__add__()to add two Money objects (same currency only)Implements
__sub__()to subtractImplements
__eq__()and__lt__()for comparisonImplements
__str__()that returns โ$100.00 USDโ
# Your code here
Exercise 5: Composition#
Create a Library system using composition:
Bookclass with title, author, ISBNLibraryclass that contains multiple booksMethods:
add_book(),remove_book(),find_by_author(),list_all()Implement
__len__()to return number of books
# Your code here
Exercise 6: Real-World Application#
Create a ShoppingCart system:
Productdataclass with name, price, categoryCartItemwith product and quantityShoppingCartclass with methods:add_item(product, quantity)remove_item(product)get_total()apply_discount(percentage)get_items_by_category(category)
from dataclasses import dataclass
# Your code here
Self-Check Quiz#
1. What is the difference between a class and an object?
Answer
A class is a blueprint/template, while an object is a specific instance created from that class.2. What does self represent?
Answer
The specific instance of the class that is calling the method.3. Whatโs the difference between instance and class variables?
Answer
Instance variables are unique to each instance (defined with self), class variables are shared by all instances.4. When should you use @classmethod vs @staticmethod?
Answer
Use @classmethod when you need access to the class (cls). Use @staticmethod when the method doesn't need access to instance or class.5. What is the purpose of init()?
Answer
Constructor method that initializes new instances of the class.6. Whatโs the difference between str() and repr()?
Answer
__str__() is for end users (readable), __repr__() is for developers (debugging, should be unambiguous).7. What is inheritance?
Answer
A mechanism where a child class inherits attributes and methods from a parent class.8. What does super() do?
Answer
Calls methods from the parent class, commonly used to call parent's __init__().9. What is polymorphism?
Answer
The ability to treat objects of different classes uniformly, often through method overriding.10. When should you use composition over inheritance?
Answer
When you want a "has-a" relationship rather than "is-a". Composition is more flexible and avoids inheritance complexity.Key Takeaways#
โ Classes are blueprints; objects are instances
โ
self refers to the current instance
โ Instance variables are unique per object; class variables are shared
โ Magic methods define how objects interact with Python operators
โ Properties provide controlled attribute access with validation
โ Inheritance creates โis-aโ relationships between classes
โ Polymorphism allows treating different objects uniformly
โ Composition creates โhas-aโ relationships (often better than inheritance)
โ Abstract base classes define interfaces that subclasses must implement
โ Dataclasses reduce boilerplate for data-focused classes
Pro Tips#
๐ก Single Responsibility Principle - Each class should do one thing well
๐ก Prefer composition over inheritance - More flexible and less coupled
๐ก Use properties for computed attributes - Cleaner than getter/setter methods
๐ก Implement repr() for debugging - Makes development easier
๐ก Use dataclasses for data containers - Less boilerplate code
๐ก Follow naming conventions - _private (convention), __dunder__ (magic)
๐ก Use ABC for interfaces - Enforce that subclasses implement required methods
๐ก Keep inheritance shallow - Deep hierarchies are hard to maintain
๐ก Type hints improve clarity - def process(self, data: List[int]) -> bool:
๐ก Document your classes - Good docstrings save time later
Common Mistakes to Avoid#
โ Forgetting self parameter - First parameter of instance methods must be self
โ
def method(self):
โ Modifying class variables unintentionally - Can affect all instances โ Use instance variables for data unique to each object
โ Not calling super().init() - Parent initialization wonโt run โ Always call parent init in subclasses
โ Using mutable default arguments - def __init__(self, items=[])
โ
Use None and create new object: items = items or []
โ Deep inheritance hierarchies - Hard to understand and maintain โ Prefer composition or shallow hierarchies
โ Ignoring repr() - Makes debugging harder โ Always implement repr for your classes
Next Steps#
You now understand Object-Oriented Programming! Next topics:
Error Handling - Try/except, custom exceptions, context managers
File I/O - Reading and writing files, working with paths
Iterators and Generators - Advanced iteration patterns
Decorators (Advanced) - Class decorators, functools
Design Patterns - Strategy, Observer, Factory, etc.
Practice Projects:
Build a library management system
Create a banking application with accounts and transactions
Design a game with characters, items, and inventory
Implement a simple e-commerce system
OOP is fundamental to modern software development - master it! ๐