Object-Oriented Programming
Chapter 8: Object-Oriented Programming
Object-Oriented Programming (OOP) organizes code around objects—bundles of data and related functions. Instead of separate variables and functions scattered throughout your code, OOP groups them into logical units called classes. This approach models real-world concepts naturally and creates reusable, maintainable code.
For example, instead of separate variables for car_make, car_model, car_year and functions drive_car(), brake_car(), you create a Car class containing all car-related data and behavior. This organization becomes essential as programs grow in complexity and scale.
Classes and Objects
A class is a blueprint for creating objects. It defines what data (attributes) and behavior (methods) objects will have. An object is a specific instance of a class—a concrete realization of the blueprint.
Here, Dog is the class (blueprint), while buddy and max are objects (instances). Each object is independent—changes to one don't affect the other.
The init Constructor
The __init__ method initializes new objects, setting their initial state. It runs automatically when you create an instance:
The __init__ method takes self (the object being created) plus any parameters you define. Inside, you set attributes using self.attribute_name = value.
Understanding self
The self parameter represents the instance itself—the specific object calling the method. Python passes it automatically; you don't provide it when calling methods:
Each instance maintains its own count value. The self parameter ensures methods operate on the correct object.
Methods: Object Behavior
Methods are functions defined inside classes. They operate on object data (attributes) and define object behavior:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
print(f"Deposited ${amount}. New balance: ${self.balance}")
def withdraw(self, amount):
if amount > self.balance:
print("Insufficient funds!")
else:
self.balance -= amount
print(f"Withdrew ${amount}. New balance: ${self.balance}")
account = BankAccount("Alice", 100)
account.deposit(50) # Deposited $50. New balance: $150
account.withdraw(30) # Withdrew $30. New balance: $120Methods access and modify object attributes through self. This encapsulation keeps related data and operations together.
Attributes: Object State
Attributes store object data. Instance attributes (set with self) are unique to each object. Class attributes (set at class level) are shared across all instances:
class Circle:
# Class attribute (shared by all circles)
pi = 3.14159
def __init__(self, radius):
# Instance attribute (unique to each circle)
self.radius = radius
def area(self):
return Circle.pi * self.radius ** 2
def circumference(self):
return 2 * Circle.pi * self.radius
c1 = Circle(5)
c2 = Circle(10)
print(c1.area()) # Uses c1's radius (5)
print(c2.area()) # Uses c2's radius (10)
print(Circle.pi) # Access class attribute through class nameUse instance attributes for data unique to each object. Use class attributes for constants or data shared across all instances.
Inheritance: Code Reuse
Inheritance lets classes inherit attributes and methods from other classes. The child (subclass) extends or modifies parent (superclass) behavior:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print(f"{self.name} makes a sound")
class Dog(Animal): # Dog inherits from Animal
def speak(self): # Override parent method
print(f"{self.name} barks: Woof!")
class Cat(Animal): # Cat inherits from Animal
def speak(self): # Override parent method
print(f"{self.name} meows: Meow!")
dog = Dog("Buddy")
cat = Cat("Whiskers")
dog.speak() # Output: Buddy barks: Woof!
cat.speak() # Output: Whiskers meows: Meow!The Dog and Cat classes inherit __init__ from Animal but provide their own speak methods. Inheritance promotes code reuse and logical organization.
Extending Parent Behavior with super()
Use super() to call parent methods while adding child-specific behavior:
class Vehicle:
def __init__(self, brand, model):
self.brand = brand
self.model = model
def info(self):
return f"{self.brand} {self.model}"
class ElectricCar(Vehicle):
def __init__(self, brand, model, battery_capacity):
super().__init__(brand, model) # Call parent constructor
self.battery_capacity = battery_capacity
def info(self):
base_info = super().info() # Get parent info
return f"{base_info} (Battery: {self.battery_capacity} kWh)"
car = ElectricCar("Tesla", "Model 3", 75)
print(car.info()) # Output: Tesla Model 3 (Battery: 75 kWh)The super() function calls parent methods, letting you extend rather than completely replace functionality.
Practical OOP Patterns
Data Modeling:
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
self.current_page = 1
def read(self, pages):
self.current_page += pages
if self.current_page > self.pages:
self.current_page = self.pages
print(f"You're on page {self.current_page} of {self.pages}")
book = Book("Python Guide", "Alice", 300)
book.read(50) # You're on page 51 of 300Composition (Has-A Relationship):
class Address:
def __init__(self, street, city, zipcode):
self.street = street
self.city = city
self.zipcode = zipcode
class Person:
def __init__(self, name, address):
self.name = name
self.address = address # Person has an Address
def info(self):
return f"{self.name} lives at {self.address.street}, {self.address.city}"
addr = Address("123 Main St", "NYC", "10001")
person = Person("Bob", addr)
print(person.info())Best Practices
- Use meaningful class names - Use nouns that describe what the class represents (Car, User, BankAccount)
- Keep classes focused - Each class should have a single, clear purpose
- Use inheritance wisely - Only inherit when there's a true "is-a" relationship (Dog is-a Animal)
- Encapsulate data - Keep attributes private when they shouldn't be modified directly
- Document with docstrings - Explain what your classes do and how to use them
OOP isn't always necessary—simple scripts work fine with functions. But as programs grow, OOP provides structure that keeps code organized, maintainable, and scalable.
Try It Yourself: Practice Exercises
Exercise 1: Rectangle Class
Create a Rectangle class with width and height attributes. Add methods for area() and perimeter().
Exercise 2: Student Class
Create a Student class with name and grades (list) attributes. Add methods to add_grade(grade) and average() that calculates the average grade.
Exercise 3: Temperature Class
Create a Temperature class that stores Celsius. Add methods to_fahrenheit() and to_kelvin() that return conversions.
Exercise 4: Inheritance Example
Create a Shape base class with a describe() method. Create Square and Triangle subclasses that inherit from Shape and override describe().
Exercise 5: Shopping Cart
Create a Product class (name, price) and ShoppingCart class. The cart should have methods to add_product(product), remove_product(name), and total() that calculates the sum.
Exercise 6: Counter with Reset
Create a Counter class with increment(), decrement(), get_value(), and reset() methods.
What's Next?
You've mastered Python's object-oriented programming fundamentals: creating classes and objects, using constructors, defining methods, working with attributes, and implementing inheritance. OOP is a powerful paradigm for organizing complex programs and modeling real-world concepts in code.
In the next chapter, we'll explore working with APIs—making HTTP requests, handling JSON data, and integrating with external services. You'll learn to fetch data from web services and build programs that interact with the broader internet.
📝 Test Your Knowledge: Object-Oriented Programming
Take this quiz to reinforce what you've learned in this chapter.