Skip to main content

OOP: Encapsulation and Polymorphism

Controlling Access and Creating Flexible Interfaces

· 3 min read

In the previous unit, we explored inheritance. Now let's look at two more OOP concepts: encapsulation and polymorphism. Encapsulation controls access to an object's data. Polymorphism lets different classes share a common interface.

OOP: Encapsulation and Polymorphism

Encapsulation

Encapsulation means bundling data with the methods that operate on it, while restricting direct access to some of that data. This prevents code outside the class from accidentally breaking the object's internal state.

class BankAccount:
def __init__(self, balance):
self.__balance = balance # Private attribute

def deposit(self, amount):
self.__balance += amount

def get_balance(self):
return self.__balance

The double underscore before balance makes it private. Code outside the class can't access __balance directly. Instead, it must use deposit() to add money and get_balance() to check the amount. This way the class controls how its data gets modified.

Private vs Protected

Python uses underscores to signal access levels:

A single underscore (like _age) marks an attribute as protected. It's a convention saying "this is internal, don't touch it from outside," but Python doesn't enforce it.

class Person:
def __init__(self):
self._age = 18 # Protected

A double underscore (like __age) marks an attribute as private. Python "mangles" the name, making it harder to access from outside the class.

class Person:
def __init__(self):
self.__age = 18 # Private

Neither is truly enforced, but double underscore provides stronger protection and signals stronger intent.

Polymorphism

Polymorphism means "many forms." It lets you write code that works with objects of different types, as long as they share a common interface.

class Animal:
def speak(self):
pass

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

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

animals = [Dog(), Cat()]

for animal in animals:
print(animal.speak()) # Outputs "Woof!" then "Meow!"

The loop doesn't care whether each animal is a Dog or Cat. It just calls speak() on each one. Each class provides its own implementation, but the calling code treats them all the same way.

Project: Encapsulated Shapes

Let's refactor our Shape class to use private attributes and accessor methods:

import turtle

class Shape:
def __init__(self, t, sides, length):
self.__t = t
self.__sides = sides
self.__length = length

def draw(self):
for _ in range(self.__sides):
self.__t.forward(self.__length)
self.__t.right(360 / self.__sides)

def get_sides(self):
return self.__sides

def get_length(self):
return self.__length

class Square(Shape):
def __init__(self, t, length):
super().__init__(t, 4, length)

class Triangle(Shape):
def __init__(self, t, length):
super().__init__(t, 3, length)

def draw_shapes(t, shapes):
for shape in shapes:
shape.draw()
t.penup()
t.forward(shape.get_length() * 2)
t.pendown()

screen = turtle.Screen()
screen.setup(width=800, height=600)
screen.bgcolor("white")

t = turtle.Turtle()

shapes = [Square(t, 50), Triangle(t, 50), Square(t, 100)]
draw_shapes(t, shapes)

t.hideturtle()
turtle.done()

The Shape class now encapsulates its attributes with double underscores. External code uses get_sides() and get_length() to read the values.

The draw_shapes function demonstrates polymorphism: it accepts any list of shapes and calls draw() on each. Whether it's a Square, Triangle, or any future shape class, the function works the same way.

In the next unit, we'll explore recursion and use it to create fractal patterns.