chapter 12.1

OOP in Practice

Encapsulation, polymorphism, composition, and @property

You know how to build classes and use inheritance. This lesson covers the design principles that separate beginner OOP from solid, maintainable OOP: encapsulation, polymorphism, composition, and properties.

Encapsulation



Encapsulation means keeping an object's internal state private and controlling access through methods. Python uses conventions (not enforcement) for this:
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance   # Single underscore: "private by convention"

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += amount

    def get_balance(self):
        return self._balance

account = BankAccount("Ada", 100)
account.deposit(50)
print(account.get_balance())  # 150

# You CAN still access _balance directly, but you shouldn't:
# print(account._balance)  # Works, but breaks the convention
◇info
_single_underscore = "private by convention" — please don't touch this from outside the class.
__double_underscore = name mangling — Python renames it to _ClassName__attr to prevent accidental access in subclasses. Rarely needed.

@property — the Pythonic way



Instead of writing get_balance() and set_balance() methods, use @property to make attributes that look like normal access but run code behind the scenes:
class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

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

    @property
    def fahrenheit(self):
        return (self._celsius * 9/5) + 32

temp = Temperature(25)
print(temp.celsius)      # 25 (calls the @property getter)
print(temp.fahrenheit)   # 77.0 (computed on the fly)
temp.celsius = 100       # Calls the setter — validation runs
print(temp.fahrenheit)   # 212.0

Polymorphism & Duck Typing



Polymorphism means different classes can have the same method name but different behavior. Python uses duck typing: if it has a .speak() method, we can call .speak() — we don't care what class it is.
class Dog:
    def speak(self):
        return "Woof!"

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

class Robot:
    def speak(self):
        return "Beep boop."

# All three have .speak() — that's all that matters
def introduce(thing):
    print(f"It says: {thing.speak()}")

for entity in [Dog(), Cat(), Robot()]:
    introduce(entity)  # Works for all three!
✓tip
"If it walks like a duck and quacks like a duck, it's a duck." Python doesn't check types — it checks capabilities. This is more flexible than Java/C++ style interfaces.

Composition over Inheritance



Inheritance says "a Dog is an Animal." Composition says "a Car has an Engine." When the relationship is "has-a" rather than "is-a", use composition:
class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower
        self.running = False

    def start(self):
        self.running = True
        print(f"Engine ({self.horsepower}hp) started")

    def stop(self):
        self.running = False
        print("Engine stopped")

class Car:
    def __init__(self, make, engine):
        self.make = make
        self.engine = engine   # Car HAS an engine (composition)

    def start(self):
        print(f"Starting {self.make}...")
        self.engine.start()

    def __str__(self):
        status = "running" if self.engine.running else "off"
        return f"{self.make} ({self.engine.horsepower}hp) — {status}"

v8 = Engine(450)
mustang = Car("Mustang", v8)
mustang.start()
print(mustang)
When to use which:
- Inheritance: the child truly *is* a specialized version of the parent (Dog is an Animal)
- Composition: the object *contains* or *uses* another object (Car has an Engine)
- Rule of thumb: prefer composition. Deep inheritance trees get tangled fast.
your turn
challenge

Create a `Vehicle` class that: 1. Uses **composition** — takes an `Engine` object and stores it 2. Has a `_fuel` attribute with a `@property` and setter that clamps between 0 and capacity 3. Has a `fuel_percent` computed property 4. Has a `drive(distance)` method that checks if the engine is running and if there's enough fuel

loading editor...

output
$ Press RUN to execute your code
AI Tutor

Ask about OOP in Practice

◇

Hey! I'm your AI tutor.

Ask me anything about this lesson, or paste code you're stuck on.

AI tutor is a premium feature