OOP Intermediate
Abstract Classes
Enforced Interfaces
Your Shape class defines area() but Shape itself can't compute an area - only
Circle and Rectangle can. Abstract classes define methods that subclasses must
implement. You can't instantiate an abstract class directly.
Basic ABC
Create an abstract base class.
# Basic Abstract Classes with ABC
from abc import ABC, abstractmethod
# Abstract class - cannot be instantiated
class Animal(ABC):
"""
Abstract base class for animals.
ABC = Abstract Base Class
"""
def __init__(self, name):
self.name = name
@abstractmethod
def speak(self):
"""All animals must implement speak()."""
pass
@abstractmethod
def move(self):
"""All animals must implement move()."""
pass
# Concrete class - implements all abstract methods
class Dog(Animal):
"""Dog implements all abstract methods."""
def speak(self):
return f"{self.name} says: Woof!"
def move(self):
return f"{self.name} runs on four legs"
class Cat(Animal):
"""Cat implements all abstract methods."""
def speak(self):
return f"{self.name} says: Meow!"
def move(self):
return f"{self.name} walks gracefully"
class Bird(Animal):
"""Bird implements all abstract methods."""
def speak(self):
return f"{self.name} says: Tweet!"
def move(self):
return f"{self.name} flies through the air"
print("=== Basic Abstract Classes ===\n")
# Creating concrete instances
dog = Dog("Buddy")
cat = Cat("Whiskers")
bird = Bird("Tweety")
# Use them normally
animals = [dog, cat, bird]
for animal in animals:
print(f"{type(animal).__name__}:")
print(f" {animal.speak()}")
print(f" {animal.move()}")
print()
# Try to instantiate abstract class
print("--- Trying to instantiate abstract class ---")
try:
animal = Animal("Generic")
except TypeError as e:
print(f"Error: {e}")
print("Cannot instantiate abstract class!")
print("\n=== Key Points ===")
print("""
1. Import: from abc import ABC, abstractmethod
2. class MyClass(ABC): makes it abstract
3. @abstractmethod marks methods that MUST be overridden
4. Cannot create instance of abstract class
5. Subclass must implement ALL abstract methods
6. If subclass doesn't implement all, it's also abstract
""")
# Demonstrate incomplete implementation
print("\n--- Incomplete implementation ---")
class Fish(Animal):
"""Fish only implements speak - incomplete!"""
def speak(self):
return f"{self.name} says: Blub!"
# Missing move() method!
try:
fish = Fish("Nemo")
except TypeError as e:
print(f"Error: {e}")
print("Fish is still abstract because move() is missing!")
from abc import ABC, abstractmethod. Class inherits from ABC.
Abstract methods
Methods that subclasses must implement.
# Defining and Implementing Abstract Methods
from abc import ABC, abstractmethod
# Abstract class with multiple abstract methods
class Shape(ABC):
"""
Abstract shape class.
Defines what all shapes must do.
"""
@abstractmethod
def area(self) -> float:
"""Calculate and return the area."""
pass
@abstractmethod
def perimeter(self) -> float:
"""Calculate and return the perimeter."""
pass
@abstractmethod
def name(self) -> str:
"""Return the name of the shape."""
pass
class Circle(Shape):
"""Concrete implementation of Shape for circles."""
PI = 3.14159
def __init__(self, radius):
self.radius = radius
def area(self) -> float:
return Circle.PI * self.radius ** 2
def perimeter(self) -> float:
return 2 * Circle.PI * self.radius
def name(self) -> str:
return "Circle"
class Rectangle(Shape):
"""Concrete implementation of Shape for rectangles."""
def __init__(self, width, height):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
def name(self) -> str:
return "Rectangle"
class Triangle(Shape):
"""Concrete implementation for triangles."""
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def area(self) -> float:
# Heron's formula
s = (self.a + self.b + self.c) / 2
return (s * (s - self.a) * (s - self.b) * (s - self.c)) ** 0.5
def perimeter(self) -> float:
return self.a + self.b + self.c
def name(self) -> str:
return "Triangle"
# Utility function that works with any Shape
def describe_shape(shape: Shape):
"""Works with any Shape - relies on abstract methods."""
print(f"Shape: {shape.name()}")
print(f" Area: {shape.area():.2f}")
print(f" Perimeter: {shape.perimeter():.2f}")
def total_area(shapes: list) -> float:
"""Calculate total area of all shapes."""
return sum(shape.area() for shape in shapes)
print("=== Abstract Methods ===\n")
# Create shapes
circle = Circle(5)
rectangle = Rectangle(4, 6)
triangle = Triangle(3, 4, 5)
shapes =
# Describe each shape
for shape in shapes:
describe_shape(shape)
print()
# Calculate total area
total = total_area(shapes)
print(f"Total area of all shapes: {total:.2f}")
# Abstract method signature documentation
print("\n--- Method Signatures ---")
print("Shape abstract methods define the interface:")
print(f" area() -> float")
print(f" perimeter() -> float")
print(f" name() -> str")
# Type checking with isinstance
print("\n--- Type Checking ---")
for shape in shapes:
print(f"{shape.name()} is a Shape: {isinstance(shape, Shape)}")
print("\n=== Why Abstract Methods? ===")
print("""
1. Enforce Contract:
- Subclasses MUST implement these methods
- Compile-time-like safety at class definition
2. Document Interface:
- Clear what methods are required
- Type hints show expected return types
3. Enable Polymorphism:
- Functions can depend on abstract methods
- Works with any concrete implementation
4. Design Patterns:
- Strategy pattern
- Template method pattern
- Factory pattern
""")
# Defining and Implementing Abstract Methods
from abc import ABC, abstractmethod
# Abstract class with multiple abstract methods
class Shape(ABC):
"""
Abstract shape class.
Defines what all shapes must do.
"""
@abstractmethod
def area(self) -> float:
"""Calculate and return the area."""
pass
@abstractmethod
def perimeter(self) -> float:
"""Calculate and return the perimeter."""
pass
@abstractmethod
def name(self) -> str:
"""Return the name of the shape."""
pass
class Circle(Shape):
"""Concrete implementation of Shape for circles."""
PI = 3.14159
def __init__(self, radius):
self.radius = radius
def area(self) -> float:
return Circle.PI * self.radius ** 2
def perimeter(self) -> float:
return 2 * Circle.PI * self.radius
def name(self) -> str:
return "Circle"
class Rectangle(Shape):
"""Concrete implementation of Shape for rectangles."""
def __init__(self, width, height):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
def name(self) -> str:
return "Rectangle"
class Triangle(Shape):
"""Concrete implementation for triangles."""
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def area(self) -> float:
# Heron's formula
s = (self.a + self.b + self.c) / 2
return (s * (s - self.a) * (s - self.b) * (s - self.c)) ** 0.5
def perimeter(self) -> float:
return self.a + self.b + self.c
def name(self) -> str:
return "Triangle"
# Utility function that works with any Shape
def describe_shape(shape: Shape):
"""Works with any Shape - relies on abstract methods."""
print(f"Shape: {shape.name()}")
print(f" Area: {shape.area():.2f}")
print(f" Perimeter: {shape.perimeter():.2f}")
def total_area(shapes: list) -> float:
"""Calculate total area of all shapes."""
return sum(shape.area() for shape in shapes)
print("=== Abstract Methods ===\n")
# Create shapes
circle = Circle(5)
rectangle = Rectangle(4, 6)
triangle = Triangle(3, 4, 5)
shapes =
# Describe each shape
for shape in shapes:
describe_shape(shape)
print()
# Calculate total area
total = total_area(shapes)
print(f"Total area of all shapes: {total:.2f}")
# Abstract method signature documentation
print("\n--- Method Signatures ---")
print("Shape abstract methods define the interface:")
print(f" area() -> float")
print(f" perimeter() -> float")
print(f" name() -> str")
# Type checking with isinstance
print("\n--- Type Checking ---")
for shape in shapes:
print(f"{shape.name()} is a Shape: {isinstance(shape, Shape)}")
print("\n=== Why Abstract Methods? ===")
print("""
1. Enforce Contract:
- Subclasses MUST implement these methods
- Compile-time-like safety at class definition
2. Document Interface:
- Clear what methods are required
- Type hints show expected return types
3. Enable Polymorphism:
- Functions can depend on abstract methods
- Works with any concrete implementation
4. Design Patterns:
- Strategy pattern
- Template method pattern
- Factory pattern
""")
# Defining and Implementing Abstract Methods
from abc import ABC, abstractmethod
# Abstract class with multiple abstract methods
class Shape(ABC):
"""
Abstract shape class.
Defines what all shapes must do.
"""
@abstractmethod
def area(self) -> float:
"""Calculate and return the area."""
pass
@abstractmethod
def perimeter(self) -> float:
"""Calculate and return the perimeter."""
pass
@abstractmethod
def name(self) -> str:
"""Return the name of the shape."""
pass
class Circle(Shape):
"""Concrete implementation of Shape for circles."""
PI = 3.14159
def __init__(self, radius):
self.radius = radius
def area(self) -> float:
return Circle.PI * self.radius ** 2
def perimeter(self) -> float:
return 2 * Circle.PI * self.radius
def name(self) -> str:
return "Circle"
class Rectangle(Shape):
"""Concrete implementation of Shape for rectangles."""
def __init__(self, width, height):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
def name(self) -> str:
return "Rectangle"
class Triangle(Shape):
"""Concrete implementation for triangles."""
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def area(self) -> float:
# Heron's formula
s = (self.a + self.b + self.c) / 2
return (s * (s - self.a) * (s - self.b) * (s - self.c)) ** 0.5
def perimeter(self) -> float:
return self.a + self.b + self.c
def name(self) -> str:
return "Triangle"
# Utility function that works with any Shape
def describe_shape(shape: Shape):
"""Works with any Shape - relies on abstract methods."""
print(f"Shape: {shape.name()}")
print(f" Area: {shape.area():.2f}")
print(f" Perimeter: {shape.perimeter():.2f}")
def total_area(shapes: list) -> float:
"""Calculate total area of all shapes."""
return sum(shape.area() for shape in shapes)
print("=== Abstract Methods ===\n")
# Create shapes
circle = Circle(5)
rectangle = Rectangle(4, 6)
triangle = Triangle(3, 4, 5)
shapes =
# Describe each shape
for shape in shapes:
describe_shape(shape)
print()
# Calculate total area
total = total_area(shapes)
print(f"Total area of all shapes: {total:.2f}")
# Abstract method signature documentation
print("\n--- Method Signatures ---")
print("Shape abstract methods define the interface:")
print(f" area() -> float")
print(f" perimeter() -> float")
print(f" name() -> str")
# Type checking with isinstance
print("\n--- Type Checking ---")
for shape in shapes:
print(f"{shape.name()} is a Shape: {isinstance(shape, Shape)}")
print("\n=== Why Abstract Methods? ===")
print("""
1. Enforce Contract:
- Subclasses MUST implement these methods
- Compile-time-like safety at class definition
2. Document Interface:
- Clear what methods are required
- Type hints show expected return types
3. Enable Polymorphism:
- Functions can depend on abstract methods
- Works with any concrete implementation
4. Design Patterns:
- Strategy pattern
- Template method pattern
- Factory pattern
""")
@abstractmethod decorator. Subclass must override or it's also abstract.
Concrete methods in ABC
Mix abstract and regular methods.
# Mixing Abstract and Concrete Methods
from abc import ABC, abstractmethod
class DataProcessor(ABC):
"""
Abstract class with both abstract and concrete methods.
Concrete methods provide reusable functionality.
"""
def __init__(self, name):
self.name = name
self.processed_count = 0
# Abstract methods - MUST be implemented
@abstractmethod
def validate(self, data) -> bool:
"""Validate the data - implementation varies by type."""
pass
@abstractmethod
def transform(self, data):
"""Transform the data - implementation varies by type."""
pass
@abstractmethod
def save(self, data):
"""Save the transformed data - implementation varies."""
pass
# Concrete methods - inherited by all subclasses
def process(self, data):
"""
Template method pattern - concrete method using abstract methods.
This orchestrates the processing workflow.
"""
print(f"[{self.name}] Starting processing...")
# Step 1: Validate
if not self.validate(data):
print(f"[{self.name}] Validation failed!")
return None
print(f"[{self.name}] Validation passed")
# Step 2: Transform
result = self.transform(data)
print(f"[{self.name}] Transformation complete")
# Step 3: Save
self.save(result)
print(f"[{self.name}] Data saved")
# Step 4: Update stats (concrete)
self.processed_count += 1
return result
def get_stats(self):
"""Concrete method - shared by all processors."""
return f"{self.name}: processed {self.processed_count} items"
class JSONProcessor(DataProcessor):
"""Processes JSON-like data (dictionaries)."""
def __init__(self):
super().__init__("JSONProcessor")
self.storage = []
def validate(self, data) -> bool:
"""JSON validation - must be dict with 'id' field."""
return isinstance(data, dict) and 'id' in data
def transform(self, data):
"""Add timestamp to JSON data."""
from datetime import datetime
transformed = data.copy()
transformed['processed_at'] = datetime.now().isoformat()
return transformed
def save(self, data):
"""Save to internal storage."""
self.storage.append(data)
class CSVProcessor(DataProcessor):
"""Processes CSV-like data (lists of values)."""
def __init__(self):
super().__init__("CSVProcessor")
self.storage = []
def validate(self, data) -> bool:
"""CSV validation - must be list with at least 2 items."""
return isinstance(data, list) and len(data) >= 2
def transform(self, data):
"""Convert all values to strings and strip whitespace."""
return [str(item).strip() for item in data]
def save(self, data):
"""Save to storage as comma-separated string."""
self.storage.append(','.join(data))
class TextProcessor(DataProcessor):
"""Processes plain text data."""
def __init__(self):
super().__init__("TextProcessor")
self.storage = []
def validate(self, data) -> bool:
"""Text validation - must be non-empty string."""
return isinstance(data, str) and len(data.strip()) > 0
def transform(self, data):
"""Normalize text - strip and uppercase."""
return data.strip().upper()
def save(self, data):
"""Save to storage."""
self.storage.append(data)
print("=== Abstract and Concrete Methods ===\n")
# Create processors
json_proc = JSONProcessor()
csv_proc = CSVProcessor()
text_proc = TextProcessor()
# Process JSON data
print("--- JSON Processing ---")
json_data = {"id": 1, "name": "Alice", "email": "alice@example.com"}
result = json_proc.process(json_data)
print(f"Result: {result}")
print()
# Process CSV data
print("--- CSV Processing ---")
csv_data = [" John ", " Doe ", " john@example.com "]
result = csv_proc.process(csv_data)
print(f"Result: {result}")
print()
# Process Text data
print("--- Text Processing ---")
text_data = " hello world "
result = text_proc.process(text_data)
print(f"Result: '{result}'")
# Test validation failure
print("\n--- Validation Failure ---")
bad_json = {"name": "No ID field"}
result = json_proc.process(bad_json)
print(f"Result: {result}")
# Stats using concrete method
print("\n--- Stats (Concrete Method) ---")
print(json_proc.get_stats())
print(csv_proc.get_stats())
print(text_proc.get_stats())
print("\n=== Template Method Pattern ===")
print("""
The process() method is a "Template Method":
1. It's concrete - defined in abstract class
2. It calls abstract methods (validate, transform, save)
3. It defines the workflow/algorithm skeleton
4. Subclasses customize by implementing abstract methods
Benefits:
- Code reuse (workflow logic is shared)
- Consistent behavior (all processors follow same steps)
- Easy to extend (just implement 3 methods)
- DRY principle (Don't Repeat Yourself)
""")
ABC can have implemented methods that subclasses inherit.
Abstract properties
Properties that must be implemented.
# Abstract Properties
from abc import ABC, abstractmethod
class Vehicle(ABC):
"""
Abstract class with abstract properties.
Properties define read-only or read-write attributes.
"""
def __init__(self, brand):
self._brand = brand
# Abstract property - must be implemented
@property
@abstractmethod
def wheels(self) -> int:
"""Number of wheels - varies by vehicle type."""
pass
@property
@abstractmethod
def vehicle_type(self) -> str:
"""Type of vehicle."""
pass
# Concrete property - shared by all
@property
def brand(self) -> str:
"""Brand is concrete - same logic for all."""
return self._brand
# Concrete method using abstract properties
def describe(self) -> str:
return f"{self.brand} {self.vehicle_type} with {self.wheels} wheels"
class Car(Vehicle):
"""Car implementation."""
@property
def wheels(self) -> int:
return 4
@property
def vehicle_type(self) -> str:
return "Car"
class Motorcycle(Vehicle):
"""Motorcycle implementation."""
@property
def wheels(self) -> int:
return 2
@property
def vehicle_type(self) -> str:
return "Motorcycle"
class Truck(Vehicle):
"""Truck implementation with variable wheels."""
def __init__(self, brand, axles):
super().__init__(brand)
self._axles = axles
@property
def wheels(self) -> int:
return self._axles * 2
@property
def vehicle_type(self) -> str:
return f"Truck ({self._axles}-axle)"
# Abstract class with read-write property
class Account(ABC):
"""
Abstract class with abstract setter.
"""
def __init__(self, owner):
self._owner = owner
self._balance = 0.0
@property
def owner(self) -> str:
return self._owner
# Abstract getter and setter
@property
@abstractmethod
def balance(self) -> float:
"""Get current balance."""
pass
@balance.setter
@abstractmethod
def balance(self, value: float):
"""Set balance - may have validation."""
pass
class SavingsAccount(Account):
"""Savings account with minimum balance."""
MINIMUM_BALANCE = 100.0
@property
def balance(self) -> float:
return self._balance
@balance.setter
def balance(self, value: float):
if value < self.MINIMUM_BALANCE:
raise ValueError(f"Balance cannot be below ${self.MINIMUM_BALANCE}")
self._balance = value
class CheckingAccount(Account):
"""Checking account with overdraft protection."""
OVERDRAFT_LIMIT = -500.0
@property
def balance(self) -> float:
return self._balance
@balance.setter
def balance(self, value: float):
if value < self.OVERDRAFT_LIMIT:
raise ValueError(f"Exceeded overdraft limit of ${-self.OVERDRAFT_LIMIT}")
self._balance = value
print("=== Abstract Properties ===\n")
# Vehicle examples
print("--- Vehicles ---")
car = Car("Toyota")
motorcycle = Motorcycle("Harley")
truck = Truck("Volvo", 3)
vehicles = [car, motorcycle, truck]
for v in vehicles:
print(f"{v.describe()}")
print(f" wheels property: {v.wheels}")
print()
# Account examples
print("--- Accounts ---")
savings = SavingsAccount("Alice")
savings.balance = 500.0
print(f"Savings: {savings.owner}, balance=${savings.balance}")
# Try to go below minimum
try:
savings.balance = 50.0
except ValueError as e:
print(f"Error: {e}")
print()
checking = CheckingAccount("Bob")
checking.balance = 100.0
print(f"Checking: {checking.owner}, balance=${checking.balance}")
# Use overdraft
checking.balance = -300.0
print(f"After overdraft: balance=${checking.balance}")
# Try to exceed overdraft
try:
checking.balance = -600.0
except ValueError as e:
print(f"Error: {e}")
print("\n=== Abstract Property Rules ===")
print("""
Syntax for abstract property:
@property
@abstractmethod
def my_property(self):
pass
Syntax for abstract setter:
@my_property.setter
@abstractmethod
def my_property(self, value):
pass
Key points:
1. @property must come BEFORE @abstractmethod
2. Subclass must implement the property
3. Can have abstract getter only, or both getter and setter
4. Concrete properties work normally
""")
Combine @property with @abstractmethod for required properties.
Multiple inheritance with ABC
Inherit from multiple abstract classes.
# Abstract Classes with Multiple Inheritance
from abc import ABC, abstractmethod
# Multiple abstract base classes
class Printable(ABC):
"""Abstract class for objects that can be printed."""
@abstractmethod
def to_string(self) -> str:
"""Convert to printable string."""
pass
def print(self):
"""Concrete method that uses to_string()."""
print(f"[PRINT] {self.to_string()}")
class Serializable(ABC):
"""Abstract class for objects that can be serialized."""
@abstractmethod
def to_dict(self) -> dict:
"""Convert to dictionary for serialization."""
pass
def to_json_string(self) -> str:
"""Concrete method that uses to_dict()."""
import json
return json.dumps(self.to_dict())
class Comparable(ABC):
"""Abstract class for objects that can be compared."""
@abstractmethod
def compare_to(self, other) -> int:
"""
Compare to another object.
Returns: negative if self < other, 0 if equal, positive if self > other
"""
pass
def __lt__(self, other):
return self.compare_to(other) < 0
def __le__(self, other):
return self.compare_to(other) <= 0
def __gt__(self, other):
return self.compare_to(other) > 0
def __ge__(self, other):
return self.compare_to(other) >= 0
def __eq__(self, other):
return self.compare_to(other) == 0
# Class inheriting from multiple abstract classes
class Product(Printable, Serializable, Comparable):
"""
Product implements all three abstract classes.
Must implement: to_string, to_dict, compare_to
"""
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
# From Printable
def to_string(self) -> str:
return f"{self.name}: ${self.price:.2f} (qty: {self.quantity})"
# From Serializable
def to_dict(self) -> dict:
return {
"name": self.name,
"price": self.price,
"quantity": self.quantity
}
# From Comparable
def compare_to(self, other) -> int:
# Compare by price
if self.price < other.price:
return -1
elif self.price > other.price:
return 1
return 0
# Another class with multiple inheritance
class Employee(Printable, Serializable):
"""Employee implements Printable and Serializable."""
def __init__(self, name, department, salary):
self.name = name
self.department = department
self.salary = salary
def to_string(self) -> str:
return f"{self.name} ({self.department}) - ${self.salary:,.2f}"
def to_dict(self) -> dict:
return {
"name": self.name,
"department": self.department,
"salary": self.salary
}
print("=== Multiple Inheritance with ABCs ===\n")
# Product examples
print("--- Products ---")
products = [
Product("Laptop", 999.99, 10),
Product("Mouse", 29.99, 50),
Product("Keyboard", 79.99, 30),
]
# Using Printable interface
print("Using Printable.print():")
for p in products:
p.print()
print()
# Using Serializable interface
print("Using Serializable.to_json_string():")
for p in products:
print(f" {p.to_json_string()}")
print()
# Using Comparable interface
print("Using Comparable (sorting by price):")
sorted_products = sorted(products)
for p in sorted_products:
print(f" {p.to_string()}")
print()
# Compare products directly
p1, p2 = products[0], products[1]
print(f"Comparing {p1.name} and {p2.name}:")
print(f" {p1.name} < {p2.name}: {p1 < p2}")
print(f" {p1.name} > {p2.name}: {p1 > p2}")
print(f" {p1.name} == {p2.name}: {p1 == p2}")
# Employee examples
print("\n--- Employees ---")
employees = [
Employee("Alice", "Engineering", 95000),
Employee("Bob", "Marketing", 75000),
]
for emp in employees:
emp.print()
print(f" JSON: {emp.to_json_string()}")
print("\n--- Method Resolution Order ---")
print(f"Product MRO: {[cls.__name__ for cls in Product.__mro__]}")
print(f"Employee MRO: {[cls.__name__ for cls in Employee.__mro__]}")
print("\n=== Multiple Inheritance Benefits ===")
print("""
1. Compose Behaviors:
- Printable: adds print capability
- Serializable: adds JSON serialization
- Comparable: adds comparison operators
2. Interface-like Pattern:
- Each ABC defines a small interface
- Classes mix and match as needed
3. Code Reuse:
- Concrete methods in ABCs provide default behavior
- Only abstract methods need implementation
4. Python's MRO (Method Resolution Order):
- Resolves diamond problem
- C3 linearization algorithm
- Use super() to delegate properly
""")
Class can implement multiple ABCs. Must implement all abstract methods.
Exercise: practical.py
Build a plugin system with abstract base classes