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