Please use a larger screen

This presentation is designed for desktop or projector displays.

← Back to curriculum

PYTHON OOP — COURSE 2

Encapsulation & Access Control

Information hiding, properties & Pythonic interfaces

Object public _protected __private

to navigate

Course Curriculum

Where we are in the journey

Agenda

Quiz Time! (Lecture 01)

What is a class in Python?

A) A variable that holds data

B) A blueprint for creating objects

C) A built-in function

D) A type of loop

A class is a blueprint that defines attributes and methods for creating objects

Recap: Lecture 01

Classes = Blueprints

  • A class defines attributes (data) and methods (behavior)
  • Objects are instances created from a class
  • Each object has its own copy of the data
class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

b1 = Book("Clean Code", "Martin")
b2 = Book("Refactoring", "Fowler")
BLUEPRINT title, author __init__() b1 b2

Quiz Time! (Lecture 01)

What does self refer to inside a method?

A) The class itself

B) The parent class

C) The current instance

D) A global variable

self refers to the specific object instance that called the method

The self Parameter

A way for an object to refer to itself

  • self is the first parameter in every instance method
  • It lets the method access the instance's attributes and other methods
  • Python passes it automatically when you call a method
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        print(f"Hi, I'm {self.name}")

p = Person("Alice", 30)
p.greet()
# Hi, I'm Alice

Quiz Time! (Lecture 01)

What is the process of creating an object from a class called?

A) Compilation

B) Instantiation

C) Inheritance

D) Encapsulation

Creating an object from a class is called instantiation — the object is an instance of the class

Quiz Time! (Lecture 01)

Which magic method is called when you use len() on an object?

A) __str__

B) __init__

C) __len__

D) __repr__

Python calls __len__ when you use the built-in len() function on an object

Quiz Time! (Lecture 01)

What's the output?

class Dog:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return f"Dog: {self.name}"

buddy = Dog("Buddy")
print(buddy)

A) Buddy

B) Dog: Buddy

C) <Dog object>

D) Error

print() calls __str__, which returns "Dog: Buddy"

OOP Timeline

The evolution of object-oriented programming

60s Simula First OOP language 70s Smalltalk Everything is an object 80-90s C++ / Java OOP goes mainstream 00s Python Multi-paradigm

[1960s] Simula

  • Created by Ole-Johan Dahl & Kristen Nygaard
  • Developed at the Norwegian Computing Center
  • Originally designed for ship simulations
  • Introduced classes, objects, inheritance and virtual methods
  • The first language to support object-oriented concepts
Simula 1967 — Norway classes + objects + inheritance Dahl & Nygaard

Simula Code Example

A BankAccount in Simula syntax (1967)

Class BankAccount;
Begin
    Real balance;

    Procedure Deposit(amount);
        Real amount;
    Begin
        balance := balance + amount;
    End;

    Procedure Withdraw(amount);
        Real amount;
    Begin
        balance := balance - amount;
    End;
End;
Already looks familiar — classes, methods, attributes!

[1970s] Smalltalk

  • Created by Alan Kay at Xerox PARC
  • Coined the term "object-oriented programming"
  • Everything is an object — even numbers, booleans, classes
  • Introduced message passing between objects
  • Influenced virtually every modern OOP language
Smalltalk 1972 — Xerox PARC "everything is an object" Alan Kay

Smalltalk Code Example

A BankAccount in Smalltalk syntax

Object subclass: #BankAccount
    instanceVariableNames: 'balance'.

BankAccount >> initialize
    balance := 0.

BankAccount >> deposit: amount
    balance := balance + amount.

BankAccount >> withdraw: amount
    balance := balance - amount.
Message passing: account deposit: 100 sends the message "deposit:" to the object

[1980s] OOP Goes Mainstream

OOP becomes the dominant paradigm in industry software development

[1990s] Standardization

OOP matures with formal standards and best practices

[2000s+] Modern Multi-Paradigm

Modern languages are multi-paradigm — OOP is one tool among many

SECTION

So... what's OOP?

Motivation & core concepts

Data — The Nouns of Software

  • Data represents things in your program
  • A user's name, age, email
  • A product's price, stock count
  • In OOP: data lives as attributes inside objects
DATA name = "Alice" age = 30 email = "a@b.com"

Behavior — The Verbs of Software

  • Behavior represents actions your program can perform
  • Login, send email, calculate total
  • In OOP: behavior lives as methods inside objects
  • Methods operate on the object's data
BEHAVIOR login() send_email() calculate()

Data + Behavior = Object

  • OOP bundles data and behavior together
  • An object knows its data and knows how to act on it
  • This bundling is called encapsulation
OBJECT Data (attributes) name, age, email Behavior (methods) login(), send_email()

Four Pillars of OOP

The core characteristics of object-oriented programming

🛡

Abstraction

Hide complexity, show essentials

🔒

Encapsulation

Bundle data & restrict access

🔁

Polymorphism

One interface, many forms

📂

Inheritance

Reuse & extend existing code

Abstraction: Car Example

🚗

What you see

  • Steering wheel
  • Gas pedal
  • Dashboard

What's hidden

  • Engine internals
  • Fuel injection system
  • Transmission gears
Abstraction = expose a simple interface, hide the complex implementation

Abstraction in Python

class Car:
    def __init__(self, model, fuel):
        self.model = model
        self._fuel = fuel

    def drive(self, distance):
        consumption = self._calculate_fuel_consumption(distance)
        self._fuel -= consumption
        print(f"Drove {distance}km, fuel left: {self._fuel}L")

    def _calculate_fuel_consumption(self, distance):
        return distance * 0.08

car = Car("Tesla", 50)
car.drive(100)  # Simple public interface
drive() is public — _calculate_fuel_consumption() is an internal detail

Abstraction: TV Example

📺

Remote Control (Interface)

  • Power on/off
  • Change channel
  • Adjust volume

🔌

Internal Circuitry (Hidden)

  • Signal processing
  • LED matrix control
  • Power regulation

Abstraction: Bank Account

🏦

Simple Interface

  • deposit()
  • withdraw()
  • get_balance()

🔒

Complex Backend

  • Fraud detection
  • Transaction logging
  • Interest calculations
Users don't need to know how it works — just what it does

Encapsulation: Information Hiding

  • Encapsulation = bundling data + methods that operate on that data
  • Information hiding = restricting direct access to internal state
  • External code interacts only through public methods
  • Internal data is protected from accidental modification
ENCAPSULATED OBJECT public methods() _protected data __private

Access Control: House Analogy

Three levels of access in Python

Public 🏠 self.name Front Yard Protected 🛋 self._name Living Room Private 🛌 self.__name Bedroom

Access Control in Python

Python uses naming conventions, not strict enforcement

Prefix Level Meaning
name Public Accessible from anywhere
_name Protected Convention: internal use only
__name Private Name mangling applied

Public Attributes

class Car:
    def __init__(self, model):
        self.model = model
        self.engine_status = "off"

    def start(self):
        self.engine_status = "running"

car = Car("Toyota")
print(car.engine_status)   # "off"
car.start()
print(car.engine_status)   # "running"
car.engine_status = "broken"  # Anyone can modify!
Public attributes can be accessed and modified by anyone — no restrictions

Protected Attributes

class Car:
    def __init__(self, model, fuel):
        self.model = model
        self._fuel_level = fuel

    def drive(self, distance):
        self._fuel_level -= distance * 0.08

    def get_fuel(self):
        return self._fuel_level

car = Car("Toyota", 50)
print(car._fuel_level)  # Works, but convention says: don't!
Single underscore _ = "I'm internal, please don't touch me directly"

Private Attributes

class Car:
    def __init__(self, model, owner):
        self.model = model
        self.__owner = owner

    def get_owner(self):
        return self.__owner

car = Car("Toyota", "Alice")
print(car.get_owner())  # "Alice"
print(car.__owner)       # AttributeError!
print(car._Car__owner)   # "Alice" (name mangling)
Double underscore __ triggers name mangling: __owner becomes _Car__owner

Access Control Comparison

Feature Public Protected _ Private __
Access from outside ✓ (discouraged) ✗ (mangled)
Access from subclass ✗ (mangled)
Enforced by Python N/A No (convention) Partially
Use case API / interface Internal logic Avoid name clashes

Getters & Setters

The traditional approach

class MyClass:
    def __init__(self, value):
        self._value = value

    def get_value(self):
        return self._value

    def set_value(self, new_value):
        if new_value >= 0:
            self._value = new_value

obj = MyClass(10)
print(obj.get_value())  # 10
obj.set_value(20)
print(obj.get_value())  # 20
Works, but not very Pythonic

The @property Decorator

Same thing, but Pythonic

class MyClass:
    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, new_value):
        if new_value >= 0:
            self._value = new_value

obj = MyClass(10)
print(obj.value)  # 10 (uses getter)
obj.value = 20    # uses setter
Looks like attribute access, but runs validation logic behind the scenes

@property: Circle Example

import math

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Radius must be positive")
        self._radius = value

    @property
    def area(self):
        return math.pi * self._radius ** 2

c = Circle(5)
print(c.area)  # 78.54 (computed property)

@property: BankAccount

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance

    @property
    def balance(self):
        return self._balance

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

    def withdraw(self, amount):
        if 0 < amount <= self._balance:
            self._balance -= amount

acc = BankAccount("Alice", 1000)
print(acc.balance)  # 1000 (read-only property)
acc.deposit(500)
print(acc.balance)  # 1500

⚠ Bypassing Protected Access

class BankAccount:
    def __init__(self, balance):
        self._balance = balance

    @property
    def balance(self):
        return self._balance

acc = BankAccount(1000)

# BAD: bypassing the interface
acc._balance = 999999
print(acc.balance)  # 999999 — no validation!
Python trusts developers — the underscore is a social contract, not a wall

Proper Usage Pattern

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

t = Temperature(100)
print(t.fahrenheit)  # 212.0
Validation in the setter + computed properties = clean, safe interface

SECTION

Modules

Organizing code into reusable files

Calculator Module Structure

  • A module = a single .py file
  • A package = a directory with __init__.py
  • Modules help organize and reuse code
calculator/
    __init__.py
    addition.py
    subtraction.py
    multiplication.py
    division.py
main.py

Module Files

addition.py

def add(a, b):
    return a + b

subtraction.py

def subtract(a, b):
    return a - b

multiplication.py

def multiply(a, b):
    return a * b

division.py

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

Using Modules: main.py

from calculator.addition import add
from calculator.subtraction import subtract
from calculator.multiplication import multiply
from calculator.division import divide

print(add(10, 5))       # 15
print(subtract(10, 5))  # 5
print(multiply(10, 5))  # 50
print(divide(10, 5))    # 2.0
Each operation lives in its own module — clean separation of concerns

Benefits of Modules

✓ Organization

Related code lives together in logical units

✓ Reusability

Import and reuse across multiple projects

✓ Namespace

Avoid naming conflicts between different parts of code

✓ Maintainability

Smaller files are easier to understand and debug

Python Standard Library

Built-in modules you'll use often

math

import math
print(math.pi)       # 3.14159...
print(math.sqrt(16))  # 4.0

datetime

from datetime import date
today = date.today()
print(today)  # 2026-03-12

os

import os
print(os.getcwd())
print(os.listdir("."))

random

import random
print(random.randint(1, 10))
print(random.choice(["a", "b"]))

OOP Drawbacks

⚠ Complexity

Class hierarchies can become deeply nested and hard to follow

⚠ Performance

Object creation and method dispatch add overhead

⚠ Development Time

Designing good class structures takes more upfront planning

⚠ Over-Engineering

Not every problem needs classes — sometimes a function is enough

Quiz Time! (Lecture 02)

What is the convention for a protected attribute in Python?

A) __name

B) _name

C) name_

D) #name

A single leading underscore _name signals that the attribute is protected (internal use)

Quiz Time! (Lecture 02)

What does the @property decorator provide?

A) Private attribute creation

B) A getter/setter interface

C) Class inheritance

D) Static method declaration

@property lets you define getter/setter methods that look like attribute access

Quiz Time! (Lecture 02)

What happens when you use __var in a class?

A) Python raises an error

B) Python renames it to _ClassName__var

C) It becomes a constant

D) It's deleted at runtime

Python applies name mangling: __var becomes _ClassName__var to avoid accidental overrides

Key Takeaways

Course Curriculum

What's next

Next up: Inheritance

Thank You!

Questions?

See you next lecture for Inheritance