Build scalable applications with classes, objects, and OOP principles
Object-Oriented Programming (OOP) is a programming paradigm that organizes code around "objects" - bundles of data and functionality. It's one of the most powerful ways to structure complex programs, making code more reusable, maintainable, and scalable.
Python's OOP capabilities enable you to model real-world entities, create custom data types, and build sophisticated applications with clean, organized code.
A class is a blueprint for creating objects. An object is an instance of a class.
# Simple class definition
class Dog:
"""A simple Dog class."""
# Class attribute (shared by all instances)
species = "Canis familiaris"
# Constructor method (runs when object is created)
def __init__(self, name, age):
# Instance attributes (unique to each object)
self.name = name
self.age = age
# Instance method
def bark(self):
return f"{self.name} says Woof!"
def get_info(self):
return f"{self.name} is {self.age} years old"
# Creating objects (instances)
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)
# Accessing attributes
print(dog1.name) # Output: Buddy
print(dog2.age) # Output: 5
print(dog1.species) # Output: Canis familiaris
# Calling methods
print(dog1.bark()) # Output: Buddy says Woof!
print(dog2.get_info()) # Output: Max is 5 years old
class BankAccount:
"""Represents a bank account with basic operations."""
# Class attribute - applies to all accounts
interest_rate = 0.02 # 2% annual interest
def __init__(self, account_number, owner, balance=0):
"""Initialize a new bank account."""
self.account_number = account_number
self.owner = owner
self.balance = balance
self.transactions = []
def deposit(self, amount):
"""Deposit money into the account."""
if amount > 0:
self.balance += amount
self.transactions.append(f"Deposit: +${amount:.2f}")
return f"Deposited ${amount:.2f}. New balance: ${self.balance:.2f}"
return "Invalid amount"
def withdraw(self, amount):
"""Withdraw money from the account."""
if amount > self.balance:
return "Insufficient funds"
if amount > 0:
self.balance -= amount
self.transactions.append(f"Withdrawal: -${amount:.2f}")
return f"Withdrew ${amount:.2f}. New balance: ${self.balance:.2f}"
return "Invalid amount"
def apply_interest(self):
"""Apply interest to the account."""
interest = self.balance * self.interest_rate
self.balance += interest
self.transactions.append(f"Interest: +${interest:.2f}")
def get_statement(self):
"""Get account statement."""
statement = f"\n=== Account Statement ===\n"
statement += f"Account: {self.account_number}\n"
statement += f"Owner: {self.owner}\n"
statement += f"Balance: ${self.balance:.2f}\n"
statement += f"\nRecent Transactions:\n"
for transaction in self.transactions[-5:]: # Last 5
statement += f" {transaction}\n"
return statement
# Usage
account = BankAccount("123456", "Alice", 1000)
print(account.deposit(500))
print(account.withdraw(200))
account.apply_interest()
print(account.get_statement())
Special methods (also called dunder methods) allow you to define how objects behave with built-in Python operations.
class Book:
"""A book with special methods."""
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __str__(self):
"""String representation for users."""
return f"'{self.title}' by {self.author}"
def __repr__(self):
"""String representation for developers."""
return f"Book('{self.title}', '{self.author}', {self.pages})"
def __len__(self):
"""Return length (number of pages)."""
return self.pages
def __eq__(self, other):
"""Check equality based on title and author."""
return (self.title == other.title and
self.author == other.author)
def __lt__(self, other):
"""Compare books by page count."""
return self.pages < other.pages
def __add__(self, other):
"""Combine pages of two books."""
return self.pages + other.pages
# Usage
book1 = Book("Python Basics", "John Doe", 250)
book2 = Book("Advanced Python", "Jane Smith", 400)
print(book1) # Calls __str__: 'Python Basics' by John Doe
print(repr(book2)) # Calls __repr__
print(len(book1)) # Calls __len__: 250
print(book1 < book2) # Calls __lt__: True
print(book1 + book2) # Calls __add__: 650
# Common special methods:
# __init__ - Constructor
# __str__ - String representation (for print)
# __repr__ - Developer representation
# __len__ - len() function
# __eq__ - == operator
# __lt__ - < operator
# __add__ - + operator
# __getitem__- [] indexing
# __call__ - Make object callable like a function
Inheritance allows a class to inherit attributes and methods from another class, promoting code reuse.
# Base class (Parent)
class Animal:
"""Base class for all animals."""
def __init__(self, name, age):
self.name = name
self.age = age
def speak(self):
"""Generic speak method."""
return "Some sound"
def get_info(self):
"""Get basic info."""
return f"{self.name} is {self.age} years old"
# Derived class (Child)
class Dog(Animal):
"""Dog class inherits from Animal."""
def __init__(self, name, age, breed):
# Call parent constructor
super().__init__(name, age)
self.breed = breed
# Override parent method
def speak(self):
return f"{self.name} barks: Woof!"
# Add new method specific to Dog
def fetch(self):
return f"{self.name} is fetching the ball"
class Cat(Animal):
"""Cat class inherits from Animal."""
def __init__(self, name, age, indoor=True):
super().__init__(name, age)
self.indoor = indoor
def speak(self):
return f"{self.name} meows: Meow!"
def scratch(self):
return f"{self.name} is scratching the furniture"
# Usage
dog = Dog("Buddy", 3, "Golden Retriever")
cat = Cat("Whiskers", 2)
print(dog.get_info()) # Inherited from Animal
print(dog.speak()) # Overridden in Dog
print(dog.fetch()) # Specific to Dog
print(cat.speak()) # Overridden in Cat
print(cat.scratch()) # Specific to Cat
class Flyer:
"""Mixin for flying ability."""
def fly(self):
return f"{self.name} is flying"
class Swimmer:
"""Mixin for swimming ability."""
def swim(self):
return f"{self.name} is swimming"
class Duck(Animal, Flyer, Swimmer):
"""Duck can fly and swim."""
def __init__(self, name, age):
super().__init__(name, age)
def speak(self):
return f"{self.name} quacks: Quack!"
# Usage
duck = Duck("Donald", 2)
print(duck.speak())
print(duck.fly())
print(duck.swim())
Encapsulation hides internal details and controls access to data using private attributes and properties.
class Employee:
"""Employee with encapsulated salary."""
def __init__(self, name, salary):
self.name = name
self._salary = salary # Protected (convention)
self.__bonus = 0 # Private (name mangling)
# Property - acts like an attribute but runs code
@property
def salary(self):
"""Getter for salary."""
return self._salary
@salary.setter
def salary(self, value):
"""Setter for salary with validation."""
if value < 0:
raise ValueError("Salary cannot be negative")
self._salary = value
@property
def annual_income(self):
"""Calculated property."""
return (self._salary * 12) + self.__bonus
def give_bonus(self, amount):
"""Give bonus (controlled access to private attribute)."""
if amount > 0:
self.__bonus = amount
# Usage
emp = Employee("Alice", 5000)
# Use property like an attribute
print(emp.salary) # Calls getter: 5000
emp.salary = 5500 # Calls setter
print(emp.salary) # 5500
# Validation in setter
try:
emp.salary = -1000 # Raises ValueError
except ValueError as e:
print(f"Error: {e}")
emp.give_bonus(2000)
print(emp.annual_income) # 68000 (5500*12 + 2000)
# Private attributes are name-mangled
# print(emp.__bonus) # AttributeError
# print(emp._Employee__bonus) # Works but don't do this!
Polymorphism allows different classes to be treated uniformly through a common interface.
class Shape:
"""Base class for shapes."""
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)
]
# Same method call works for all shapes
for shape in shapes:
print(f"{shape.__class__.__name__}:")
print(f" Area: {shape.area():.2f}")
print(f" Perimeter: {shape.perimeter():.2f}")
from datetime import datetime, timedelta
class LibraryItem:
"""Base class for library items."""
def __init__(self, title, item_id):
self.title = title
self.item_id = item_id
self.checked_out = False
self.due_date = None
def checkout(self, days=14):
"""Check out the item."""
if self.checked_out:
return f"{self.title} is already checked out"
self.checked_out = True
self.due_date = datetime.now() + timedelta(days=days)
return f"Checked out: {self.title}. Due: {self.due_date.strftime('%Y-%m-%d')}"
def return_item(self):
"""Return the item."""
if not self.checked_out:
return f"{self.title} is not checked out"
self.checked_out = False
was_late = datetime.now() > self.due_date
self.due_date = None
return "Late return!" if was_late else "Returned on time"
class Book(LibraryItem):
def __init__(self, title, item_id, author, pages):
super().__init__(title, item_id)
self.author = author
self.pages = pages
def __str__(self):
status = "Checked Out" if self.checked_out else "Available"
return f"Book: '{self.title}' by {self.author} ({pages} pages) - {status}"
class DVD(LibraryItem):
def __init__(self, title, item_id, director, duration):
super().__init__(title, item_id)
self.director = director
self.duration = duration
def checkout(self, days=7): # DVDs have shorter checkout period
return super().checkout(days)
def __str__(self):
status = "Checked Out" if self.checked_out else "Available"
return f"DVD: '{self.title}' by {self.director} ({self.duration} min) - {status}"
class Library:
"""Manages library items."""
def __init__(self, name):
self.name = name
self.items = []
def add_item(self, item):
"""Add item to library."""
self.items.append(item)
print(f"Added: {item.title}")
def find_item(self, title):
"""Find item by title."""
for item in self.items:
if item.title.lower() == title.lower():
return item
return None
def list_available(self):
"""List all available items."""
available = [item for item in self.items if not item.checked_out]
if not available:
print("No items available")
return
print(f"\nAvailable items at {self.name}:")
for item in available:
print(f" - {item}")
# Usage
library = Library("City Library")
# Add items
library.add_item(Book("Python Crash Course", "B001", "Eric Matthes", 544))
library.add_item(Book("Clean Code", "B002", "Robert Martin", 464))
library.add_item(DVD("The Matrix", "D001", "Wachowskis", 136))
# Check out item
book = library.find_item("Python Crash Course")
if book:
print(book.checkout())
# List available
library.list_available()
With OOP mastered, you're ready for:
Test your understanding of Object-Oriented Programming!