Day 14: OOP — Inheritance, Encapsulation & Polymorphism

🐍 Day 14: OOP — Inheritance, Encapsulation & Polymorphism


1. Learning Objectives

By the end of Day 14, you will be able to:

  • Use inheritance to create child classes that reuse and extend parent functionality
  • Override methods and use super() to call parent methods
  • Apply encapsulation to control access to attributes (private/protected conventions)
  • Write properties with the @property decorator for controlled access
  • Understand polymorphism — different classes sharing a common interface
  • Design object‑oriented systems that are flexible, maintainable, and safe

2. Concept Explanation

2.1 Inheritance — Building on What Already Exists

In real life, a sedan is a type of car, which is a type of vehicle. Inheritance models these "is‑a" relationships. A child class inherits all attributes and methods from its parent, then adds or modifies as needed.

class Vehicle:                    # Parent (base) class
    def __init__(self, brand):
        self.brand = brand

    def start(self):
        print(f"{self.brand} vehicle started.")

class Car(Vehicle):               # Child class inherits from Vehicle
    def __init__(self, brand, model):
        super().__init__(brand)   # Call parent's __init__
        self.model = model

    def honk(self):
        print("Beep beep!")
  • Car inherits start() from Vehicle.
  • super().__init__(brand) runs the parent’s constructor, so self.brand is set correctly.

2.2 Method Overriding & super()

A child class can override (replace) a parent method:

class ElectricCar(Car):
    def start(self):
        print(f"{self.brand} {self.model} starts silently.")

my_tesla = ElectricCar("Tesla", "Model 3")
my_tesla.start()   # "Tesla Model 3 starts silently."

If you still need the parent’s version, call super():

class HybridCar(Car):
    def start(self):
        super().start()               # Runs Car's start (which runs Vehicle's)
        print("Electric motor engaged.")

2.3 Encapsulation — Protecting Your Data

Encapsulation hides internal details and prevents accidental changes. Python uses naming conventions, not strict enforcement, to signal privacy:

ConventionMeaning
self._valueProtected — "please don't touch, but you can if you know what you're doing"
self.__valuePrivate — name mangling makes it harder to access from outside (but still possible)
class BankAccount:
    def __init__(self, owner):
        self.owner = owner
        self._balance = 0          # protected
        self.__pin = "1234"        # private (name mangled to _BankAccount__pin)

    def deposit(self, amount):
        if amount > 0:
            self._balance += amount

    def get_balance(self):
        return self._balance

Accessing self.__pin from outside raises AttributeError (unless you use the mangled name). This discourages direct access.


2.4 Properties: Controlled Attribute Access

Instead of writing get_balance() and set_balance() methods manually, use properties with the @property decorator:

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        """Getter — read the temperature"""
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        """Setter — validate before changing"""
        if value < -273.15:
            raise ValueError("Below absolute zero!")
        self._celsius = value

    @property
    def fahrenheit(self):
        """Computed property — no setter needed"""
        return self._celsius * 9/5 + 32

Usage:

t = Temperature(25)
print(t.celsius)      # 25        (calls getter)
t.celsius = 30        # (calls setter, validation)
print(t.fahrenheit)   # 86.0      (computed)
# t.fahrenheit = 100  # AttributeError: no setter defined

Properties make your code Pythonic and safe.


2.5 Polymorphism — One Interface, Many Forms

Polymorphism means different classes can be used interchangeably if they share the same method names. Python relies on duck typing: "If it walks like a duck and quacks like a duck, it's a duck."

class Dog:
    def speak(self):
        return "Woof!"

class Cat:
    def speak(self):
        return "Meow!"

def animal_sound(animal):
    print(animal.speak())

animal_sound(Dog())   # Woof!
animal_sound(Cat())   # Meow!

The animal_sound() function doesn't care what type animal is — only that it has a speak() method. This is powerful for writing flexible code.


2.6 When to Use What

FeatureUse Case
InheritanceWhen a class is a specialised version of another (e.g., StudentGraduateStudent)
Composition"Has‑a" relationship — often better than deep inheritance chains (e.g., Library has Books, not Library is a Book)
EncapsulationProtect internal state, expose only safe interfaces
PropertiesAdd validation or computed values without breaking existing code
PolymorphismWhen you want to process objects generically based on their behaviour

3. Code Examples

Example 1: Inheritance Hierarchy

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def work(self):
        return f"{self.name} is working."

class Manager(Employee):
    def __init__(self, name, salary, department):
        super().__init__(name, salary)
        self.department = department

    def work(self):
        return f"{self.name} is managing the {self.department} department."

class Developer(Employee):
    def __init__(self, name, salary, language):
        super().__init__(name, salary)
        self.language = language

    def work(self):
        return f"{self.name} is coding in {self.language}."

# Polymorphism in action
team = [Manager("Alice", 90000, "Sales"),
        Developer("Bob", 80000, "Python"),
        Developer("Charlie", 85000, "JavaScript")]

for member in team:
    print(member.work())

Example 2: Encapsulation with Properties

class Person:
    def __init__(self, name, age):
        self.name = name
        self._age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if not (0 <= value <= 150):
            raise ValueError("Invalid age")
        self._age = value

p = Person("Alice", 28)
print(p.age)     # 28
p.age = 30       # valid
# p.age = -5     # raises ValueError

Example 3: Composition over Inheritance

class Battery:
    def __init__(self, capacity):
        self.capacity = capacity

    def info(self):
        return f"{self.capacity} kWh battery"

class ElectricCar:
    def __init__(self, brand, battery):
        self.brand = brand
        self.battery = battery   # composition: has a battery

b = Battery(75)
car = ElectricCar("Tesla", b)
print(car.battery.info())   # 75 kWh battery

4. Hands-On Exercises

Exercise 1: Animal Inheritance

Create a base class Animal with a make_sound() method (just pass). Then create Dog and Cat subclasses that override make_sound() to return "Woof" and "Meow". Demonstrate polymorphism by iterating over a list of animals.

Exercise 2: Shape Hierarchy

Define a Shape class with an area() method that returns 0. Create Rectangle(width, height) and Circle(radius) subclasses that override area(). Use math.pi for the circle. Create a list of shapes and print their areas.

Exercise 3: Encapsulation with Account

Build a BankAccount class with a private __balance attribute (using name mangling). Provide deposit(amount) and withdraw(amount) methods. Add a property balance that only provides a getter (no setter). Test that direct assignment to balance raises an error.

Exercise 4: Property Validation

Create a Product class with name and _price. Use a property for price that validates: price must be >= 0. Raise ValueError if negative.

Exercise 5: Polymorphic Function

Write a function describe(vehicle) that prints the result of a description() method. Create Car and Bicycle classes both with description(). Show that the function works with both.


5. Applied Challenge Task 🏗️

Employee Management System

Design an OOP‑based employee management system that demonstrates inheritance, encapsulation, and polymorphism.

Requirements:

  1. Base class Employee:

    • Attributes: name (public), _salary (protected)
    • Property salary with getter and a setter that ensures salary is never negative.
    • Method work() that returns a generic string like "Employee is working."
  2. Subclass Manager(Employee):

    • Additional attribute department (public)
    • Override work() to return "{name} is managing the {department} department."
    • Add a method hold_meeting() that prints "{name} is holding a meeting."
  3. Subclass Developer(Employee):

    • Additional attribute programming_language (public)
    • Override work() to return "{name} is writing code in {programming_language}."
  4. Class Company:

    • Holds a list of employees (composition).
    • Methods: add_employee(employee), list_all_employees(), total_salary_expense() (sum of all salaries).
  5. Menu‑driven interface:

    • Add Manager / Add Developer
    • List all employees (showing their work() result)
    • Show total salary expense
    • Quit

Stretch goals:

  • Add CSV or JSON file persistence (load/store company data)
  • Add an Intern(Employee) subclass with a fixed salary (no setter)
  • Use @classmethod to create an alternative constructor (e.g., Developer.from_string("Bob,85000,Python"))

6. Brief Review Summary

ConceptKey Points
InheritanceChild class inherits from parent; "is‑a" relationship
super()Calls parent methods, especially in __init__
Method overridingChild provides its own version of a parent method
EncapsulationUse _ (protected) and __ (private) conventions; prefer properties
@propertyTurns a method into an attribute with getter/setter/deleter
PolymorphismDifferent objects respond to the same method call; duck typing
Composition"Has‑a" relationship — often more flexible than inheritance

7. Preview of Next Topic — Day 15

Tomorrow we wrap up Phase 2 with a Weekly Mini‑Project:

  • An OOP‑based system (e.g., inventory, banking, task manager) that combines all Phase 2 concepts
  • Apply classes, inheritance, encapsulation, polymorphism, file handling, exception handling
  • Emphasis on clean architecture and reusability

🎯 Your Action Items for Day 14:

  1. ✅ Complete all 5 exercises
  2. ✅ Build the Employee Management System challenge
  3. ✅ Experiment with super() – call a parent method before/after your custom code
  4. ✅ Try creating a private attribute __secret and access it from outside using the mangled name

Comments

Popular posts from this blog

Day 1: Welcome to Python — Your Journey Begins

Python: Your Gateway to Coding Adventures

Day 11: List Comprehensions & Lambda Functions

Earn From the Comfort of Your Home

Build the skills to work comfortably from home, on your own terms.

Get Started Today