Learning Objectives: After this lesson, you'll understand the fundamentals of OOP in Python - classes, objects, attributes, and methods - and learn to build your first Python classes with proper encapsulation.
What is Object-Oriented Programming?
Object-Oriented Programming (OOP) is a programming paradigm that organizes code around objects - entities that contain both data (attributes) and functions (methods) that operate on that data.
Think of objects like real-world entities:
- A car has properties (color, model, speed) and behaviors (start, stop, accelerate)
- A bank account has properties (balance, owner) and behaviors (deposit, withdraw, check_balance)
- A student has properties (name, grades) and behaviors (study, take_exam, calculate_gpa)
Loading Python runtime...
Classes and Objects
A class is a blueprint for creating objects. An object is an instance of a class.
Loading Python runtime...
Interactive OOP Playground
Now let's visualize how OOP works with an interactive playground! Create objects, call methods, and see how each object maintains its own state:
Class Anatomy
Let's break down the parts of a class:
<OOPInheritanceTree initialClasses={[ { "id": "bankaccount", "name": "BankAccount", "methods": ["init", "deposit", "withdraw", "get_balance"], "attributes": ["owner", "balance"], "level": 0 } ]} />
Loading Python runtime...
Instance vs Class Variables
Understanding the difference between instance and class variables:
Loading Python runtime...
Methods and Self
The self parameter is crucial in Python classes:
Loading Python runtime...
Encapsulation and Privacy
Python uses naming conventions to indicate privacy:
Loading Python runtime...
Special Methods (Magic Methods)
Python classes can define special methods that enable built-in operations:
Loading Python runtime...
Practical Examples
Example 1: Student Management System
Loading Python runtime...
Example 2: Library Management System
Loading Python runtime...
Key Takeaways
✅ Classes are blueprints for creating objects with attributes and methods
✅ Objects are instances of classes with their own data
✅ init method initializes new objects (constructor)
✅ self parameter refers to the current instance
✅ Instance variables are unique to each object
✅ Class variables are shared by all instances
✅ Encapsulation uses naming conventions (_protected, __private)
✅ Special methods enable built-in operations (__str__, __len__, etc.)
Connections: OOP Across Programming and Software Design
🔗 Connection to Real-World Modeling
OOP was designed to model the real world in code:
Physical Objects → Classes:
class Car: # Properties (what it has) wheels = 4 def __init__(self, make, model): self.make = make self.model = model self.speed = 0 # Behaviors (what it can do) def accelerate(self): self.speed += 10 def brake(self): self.speed = max(0, self.speed - 10) # Real car → Object instance my_car = Car("Toyota", "Camry") my_car.accelerate() # Car speeds up
Systems → Object Interactions:
# Restaurant system class Restaurant: def __init__(self): self.menu = Menu() self.kitchen = Kitchen() self.tables = [Table(i) for i in range(10)] def take_order(self, table_num, items): order = Order(items) self.tables[table_num].add_order(order) self.kitchen.prepare(order)
Real-world relationships:
- Has-a (Composition): Car has-a Engine
- Is-a (Inheritance): Dog is-a Animal
- Uses-a (Dependency): Driver uses-a Car
🔗 Connection to Other Programming Paradigms
Three Main Paradigms:
- Procedural (functions and data separate)
# Procedural def calculate_area(length, width): return length * width room_length = 10 room_width = 8 area = calculate_area(room_length, room_width)
- Object-Oriented (data and functions together)
# OOP class Rectangle: def __init__(self, length, width): self.length = length self.width = width def calculate_area(self): return self.length * self.width room = Rectangle(10, 8) area = room.calculate_area()
- Functional (pure functions, immutable data)
# Functional from dataclasses import dataclass @dataclass(frozen=True) # Immutable class Rectangle: length: int width: int def calculate_area(rectangle): return rectangle.length * rectangle.width room = Rectangle(10, 8) area = calculate_area(room)
When to use each:
- Procedural: Simple scripts, data processing
- OOP: Complex systems, modeling real entities
- Functional: Data transformations, parallel processing
🔗 Connection to Software Design Principles
SOLID Principles (fundamental OOP design):
S - Single Responsibility Principle:
# Bad: Class does too much class User: def __init__(self, name): self.name = name def save_to_database(self): # Database responsibility pass def send_email(self): # Email responsibility pass # Good: Each class has one responsibility class User: def __init__(self, name): self.name = name class UserRepository: def save(self, user): pass # Handle database class EmailService: def send(self, user, message): pass # Handle email
O - Open/Closed Principle:
# Open for extension, closed for modification class Shape: def area(self): pass class Rectangle(Shape): def __init__(self, w, h): self.width, self.height = w, h def area(self): return self.width * self.height class Circle(Shape): def __init__(self, r): self.radius = r def area(self): return 3.14 * self.radius ** 2 # Can add new shapes without modifying existing code!
L - Liskov Substitution Principle:
# Subclasses should be substitutable for their base classes class Bird: def move(self): return "Flying" class Penguin(Bird): def move(self): return "Swimming" # Different implementation, same interface def relocate_bird(bird: Bird): print(f"Bird is {bird.move()}") relocate_bird(Bird()) # Works relocate_bird(Penguin()) # Also works! Substitutable
🔗 Connection to Design Patterns
Common OOP Patterns:
Factory Pattern (Create objects without specifying exact class):
class AnimalFactory: @staticmethod def create_animal(animal_type): if animal_type == "dog": return Dog() elif animal_type == "cat": return Cat() else: return Animal() # Usage pet = AnimalFactory.create_animal("dog")
Singleton Pattern (Only one instance exists):
class Database: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance # Always returns same instance db1 = Database() db2 = Database() print(db1 is db2) # True
Observer Pattern (Objects notify observers of changes):
class Subject: def __init__(self): self._observers = [] def attach(self, observer): self._observers.append(observer) def notify(self, message): for observer in self._observers: observer.update(message) class Observer: def update(self, message): print(f"Received: {message}")
🔗 Connection to Other Languages
OOP in Different Languages:
| Feature | Python | JavaScript | Java | C++ |
|---|---|---|---|---|
| Define class | class Dog: | class Dog { | class Dog { | class Dog { |
| Constructor | __init__(self) | constructor() | Dog() | Dog() |
| Inheritance | class Dog(Animal): | class Dog extends Animal | class Dog extends Animal | class Dog : public Animal |
| Multiple inheritance | ✅ Yes | ❌ No | ❌ No | ✅ Yes |
| Private members | __name convention | #name (ES2022) | private String name | private: string name |
| Type declarations | Optional (type hints) | Optional (TypeScript) | Required | Required |
Python's OOP Advantages:
- Simple, readable syntax
- Multiple inheritance support
- Duck typing ("if it quacks like a duck...")
- Dynamic attribute addition
🔗 Connection to Memory and Performance
How Objects Live in Memory:
Python Memory: ┌──────────────────────────────┐ │ Class Definition (Blueprint) │ │ BankAccount │ │ - __init__, deposit, etc │ └──────────────────────────────┘ ↓ creates instances ┌──────────────────────────────┐ │ Object 1 (Instance) │ │ owner: "Alice" │ │ balance: 1000 │ │ id: 0x7f8b2c3d4e50 │ └──────────────────────────────┘ ┌──────────────────────────────┐ │ Object 2 (Instance) │ │ owner: "Bob" │ │ balance: 500 │ │ id: 0x7f8b2c3d4f60 │ └──────────────────────────────┘
Memory Optimization:
# Without __slots__: Each instance has its own __dict__ class Point: def __init__(self, x, y): self.x = x self.y = y # ~200 bytes per instance # With __slots__: Fixed attributes, less memory class Point: __slots__ = ['x', 'y'] def __init__(self, x, y): self.x = x self.y = y # ~100 bytes per instance (50% savings!)
🔗 Connection to Data Structures and Algorithms
OOP enables complex data structures:
Linked List Node:
class Node: def __init__(self, data): self.data = data self.next = None class LinkedList: def __init__(self): self.head = None def append(self, data): new_node = Node(data) if not self.head: self.head = new_node return current = self.head while current.next: current = current.next current.next = new_node
Binary Tree Node:
class TreeNode: def __init__(self, value): self.value = value self.left = None self.right = None def insert(self, value): if value < self.value: if self.left is None: self.left = TreeNode(value) else: self.left.insert(value) else: if self.right is None: self.right = TreeNode(value) else: self.right.insert(value)
🔗 Connection to Modern Software Architecture
Microservices (OOP at system level):
# Each service is like an object with specific responsibilities class UserService: def authenticate(self, credentials): pass def get_profile(self, user_id): pass class PaymentService: def process_payment(self, amount): pass def refund(self, transaction_id): pass class OrderService: def create_order(self, items): pass def track_order(self, order_id): pass
APIs (Objects communicating over network):
# RESTful API resources model objects class ProductAPI: def get(self, product_id): # GET /products/123 return Product.find(product_id) def create(self, data): # POST /products return Product.create(data) def update(self, product_id, data): # PUT /products/123 product = Product.find(product_id) product.update(data) return product
🔗 Connection to Game Development
OOP is fundamental to game programming:
class GameObject: def __init__(self, x, y): self.x = x self.y = y self.velocity_x = 0 self.velocity_y = 0 def update(self, delta_time): self.x += self.velocity_x * delta_time self.y += self.velocity_y * delta_time def draw(self, screen): pass # Override in subclasses class Player(GameObject): def __init__(self, x, y): super().__init__(x, y) self.health = 100 self.score = 0 def jump(self): self.velocity_y = -10 class Enemy(GameObject): def __init__(self, x, y): super().__init__(x, y) self.health = 50 def chase_player(self, player): if player.x > self.x: self.velocity_x = 2 else: self.velocity_x = -2
🔗 Connection to Testing
OOP makes testing easier:
# Original class class EmailService: def send(self, to, message): # Actually send email pass # Mock for testing (Test Double) class MockEmailService(EmailService): def __init__(self): self.sent_emails = [] def send(self, to, message): self.sent_emails.append((to, message)) # Don't actually send, just record # Test def test_user_registration(): email_service = MockEmailService() user_service = UserService(email_service) user_service.register("alice@example.com") assert len(email_service.sent_emails) == 1 assert email_service.sent_emails[0][0] == "alice@example.com"
🔗 Historical Evolution
OOP Timeline:
- 1960s: Simula introduces classes and objects
- 1970s: Smalltalk develops pure OOP
- 1980s: C++ brings OOP to mainstream
- 1991: Python created with OOP support
- 1995: Java and JavaScript popularize OOP
- 2000s: OOP becomes dominant paradigm
- 2010s+: Functional programming gains popularity, OOP evolves
Python's OOP Philosophy:
- "Everything is an object" (even functions!)
- Dynamic typing with duck typing
- Multiple inheritance with MRO (Method Resolution Order)
- Explicit self (unlike other languages' implicit this)
Remember: OOP isn't just about syntax - it's a way of thinking about problems. Model your programs after the real world, and your code becomes more intuitive, maintainable, and powerful!
Next Steps
In the next lesson, we'll learn about error handling and debugging - how to handle errors gracefully with try-except blocks, understand common exceptions, and master debugging techniques to make your code more robust.
Ready to make your code bulletproof? The next lesson will teach you professional error handling techniques!