Introduction to Object-Oriented Programming

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:

Loading interactive component...

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:

  1. 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)
  1. 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()
  1. 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:

FeaturePythonJavaScriptJavaC++
Define classclass Dog:class Dog {class Dog {class Dog {
Constructor__init__(self)constructor()Dog()Dog()
Inheritanceclass Dog(Animal):class Dog extends Animalclass Dog extends Animalclass Dog : public Animal
Multiple inheritance✅ Yes❌ No❌ No✅ Yes
Private members__name convention#name (ES2022)private String nameprivate: string name
Type declarationsOptional (type hints)Optional (TypeScript)RequiredRequired

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!