You're building a game. You have players, enemies, weapons, items. Each has properties (health, damage, position). Each can perform actions (move, attack, defend).
How do you organize this in code?
You could use separate variables for everything: player1_health = 100, player1_x = 50, player1_y = 30, player2_health = 100, player2_x = 80, player2_y = 20, enemy1_health = 50, enemy1_x = 100, ...
This gets messy fast. Hundreds of variables. Hard to track. Hard to modify. Hard to understand.
Object-Oriented Programming (OOP) solves this. Instead of thinking in isolated variables and functions, you think in objects—things that have properties and behaviors, just like real-world objects.
A player is an object. It has health. It has a position. It can move. It can attack.
An enemy is an object. Same properties and behaviors.
OOP organizes code the way humans naturally think about the world—in terms of things and what they do.
Understanding OOP changes everything. You start to see programs as collections of interacting objects. You recognize relationships between components. You design reusable, maintainable systems.
This isn't just a programming technique. It's a fundamental way of thinking about software design—used in virtually every modern application.
Let's explore how to model the world in code.
What Are Classes and Objects? Definition: A class is a blueprint or template that defines the structure and behavior of objects. An object is a specific instance of a class.
Analogy: Class: Cookie cutter (the template); Object: Individual cookies (made from template). The cookie cutter defines the shape. But you can make many cookies from one cutter. Each cookie is a separate object.
Example: Student class
class Student:
def __init__(self, name, student_id, major):
self.name = name
self.student_id = student_id
self.major = major
self.gpa = 0.0
def study(self, hours):
print(f"{self.name} studied for {hours} hours")
def get_info(self):
return f"Student: {self.name}, ID: {self.student_id}, Major: {self.major}, GPA: {self.gpa}"
Class components: Attributes (properties): name, student_id, major, gpa; Methods (behaviors): study(), get_info()
alice = Student("Alice Johnson", "S12345", "Computer Science")
bob = Student("Bob Smith", "S12346", "Mathematics")
print(alice.name) # Alice Johnson
print(bob.name) # Bob Smith
alice.study(3) # Alice Johnson studied for 3 hours
bob.study(2) # Bob Smith studied for 2 hours
alice.gpa = 3.8
bob.gpa = 3.5
print(alice.get_info())
# Student: Alice Johnson, ID: S12345, Major: Computer Science, GPA: 3.8
Key point: Alice and Bob are separate objects. Changing Alice's GPA doesn't affect Bob's.
The "self" Parameter: "self" refers to the current instance of the class, allowing access to its attributes and methods. When you call alice.introduce(), self refers to alice. When you call bob.introduce(), self refers to bob.
Definition: A constructor is a special method that initializes a new object when it's created, setting up initial state and performing setup tasks.
In Python, __init__ is the constructor:
class BankAccount:
def __init__(self, account_number, owner, initial_balance=0):
self.account_number = account_number
self.owner = owner
self.balance = initial_balance
print(f"Account created for {owner}")
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
return True
return False
account1 = BankAccount("ACC001", "Alice", 1000)
account2 = BankAccount("ACC002", "Bob")
Constructor Overloading: Some languages (like Java, C++) allow multiple constructors. Python doesn't have constructor overloading, but achieves similar effect with default parameters.
Definition: Instance members belong to specific objects. Each object has its own copy.
class Car:
def __init__(self, brand, model):
self.brand = brand # Instance variable
self.model = model # Instance variable
self.odometer = 0 # Instance variable
def drive(self, miles):
self.odometer += miles
car1 = Car("Toyota", "Camry")
car2 = Car("Honda", "Civic")
car1.drive(100)
car2.drive(50)
print(car1.odometer) # 100
print(car2.odometer) # 50
Definition: Static members belong to the class itself, not individual objects. All objects share the same static member.
class Car:
total_cars = 0 # Static variable (class variable)
def __init__(self, brand, model):
self.brand = brand
self.model = model
Car.total_cars += 1
@staticmethod
def get_total_cars():
return Car.total_cars
car1 = Car("Toyota", "Camry")
car2 = Car("Honda", "Civic")
car3 = Car("Ford", "Mustang")
print(Car.total_cars) # 3
print(Car.get_total_cars()) # 3
Use cases for static members: Constants (PI shared by all circles), Counters (track total objects created), Utility functions (operations that don't need object data).
Definition: Encapsulation is the bundling of data and methods that operate on that data within a class, restricting direct access to some components to protect object integrity.
Python convention (weak encapsulation): Public: self.name; Private (by convention): self._name (single underscore); Strongly private: self.__name (double underscore, name mangling)
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # Public
self._account_number = "..." # Protected (convention)
self.__balance = balance # Private
def deposit(self, amount):
if amount > 0:
self.__balance += amount
def get_balance(self):
return self.__balance
account = BankAccount("Alice", 1000)
print(account.owner) # OK (public)
print(account.get_balance()) # OK (via method)
# print(account.__balance) # ERROR! Private
Why Encapsulation? Data validation (can validate age before setting), Flexibility (can change internal implementation without breaking code), Security (hide sensitive data from external access).
Definition: Inheritance is a mechanism where a new class (subclass/child) is derived from an existing class (superclass/parent), inheriting its attributes and methods while potentially adding or modifying behavior.
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def make_sound(self):
print("Some generic animal sound")
def sleep(self):
print(f"{self.name} is sleeping")
class Dog(Animal):
def __init__(self, name, age, breed):
super().__init__(name, age)
self.breed = breed
def make_sound(self):
print("Woof! Woof!")
def fetch(self):
print(f"{self.name} is fetching the ball")
dog = Dog("Buddy", 3, "Golden Retriever")
dog.sleep() # Buddy is sleeping
dog.make_sound() # Woof! Woof!
dog.fetch() # Buddy is fetching the ball
The "super()" Function: super() calls methods from the parent class, typically used in constructors and when extending parent behavior.
Benefits of Inheritance: Code reuse (don't rewrite common functionality), Logical hierarchy (models real-world relationships), Polymorphism (treat different objects similarly), Extensibility (add features without modifying existing code).
Definition: Polymorphism is the ability of different objects to respond to the same method call in their own way, allowing one interface to be used for different underlying forms. Literally: "Many shapes"
class Shape:
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
shapes = [Rectangle(5,10), Circle(7), Rectangle(3,8)]
for shape in shapes:
print(f"Area: {shape.area()}")
Benefits of Polymorphism: Flexibility (add new types without changing existing code), Simplified code (single interface for multiple types), Extensibility (new subclasses integrate seamlessly), Maintainability (changes localized to specific classes).
Definition: An abstract class is a class that cannot be instantiated and is designed to be inherited by subclasses that implement its abstract methods. Purpose: Define a contract—what methods subclasses must implement.
from abc import ABC, abstractmethod
class Animal(ABC):
def __init__(self, name):
self.name = name
@abstractmethod
def make_sound(self):
pass
@abstractmethod
def move(self):
pass
def sleep(self):
print(f"{self.name} is sleeping")
class Dog(Animal):
def make_sound(self):
return "Woof!"
def move(self):
return "Running on four legs"
dog = Dog("Buddy")
print(dog.make_sound()) # Woof!
dog.sleep() # Buddy is sleeping
Why Abstract Classes? Enforce interface (guarantee subclasses implement required methods), Documentation (clear contract), Prevent instantiation (can't create incomplete objects), Shared functionality (provide common implementation).
Definition: Composition is a strong "has-a" relationship where objects are composed of other objects, and the contained objects cannot exist independently.
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
print("Engine started")
class Car:
def __init__(self, brand, model, horsepower):
self.brand = brand
self.model = model
self.engine = Engine(horsepower) # Car HAS-A Engine
car = Car("Toyota", "Camry", 200)
car.engine.start() # Engine started
Key characteristic: Engine is part of Car. If Car is destroyed, Engine is destroyed too.
Definition: Aggregation is a weaker "has-a" relationship where objects are associated but can exist independently.
class Employee:
def __init__(self, name):
self.name = name
class Department:
def __init__(self, name):
self.name = name
self.employees = []
def add_employee(self, employee):
self.employees.append(employee)
alice = Employee("Alice")
it_dept = Department("IT")
it_dept.add_employee(alice) # Department HAS employees
Key characteristic: Employees can exist without Department.
Composition vs Aggregation: Composition has strong relationship where contained object dies with container; Aggregation has weak relationship where contained object can exist independently. Examples: Car-Engine (composition), Department-Employees (aggregation).
Definition: Design patterns are reusable solutions to commonly occurring problems in software design, providing templates for how to solve problems in various contexts.
Problem: Need exactly one instance of a class (e.g., configuration manager, database connection).
class DatabaseConnection:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.connected = False
return cls._instance
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # True (same object)
Use cases: Configuration managers, logging, caching, database connections.
Problem: 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()
animal = AnimalFactory.create_animal("dog")
Use cases: Object creation based on configuration, plugin systems, GUI components.
Problem: Multiple objects need to be notified when something 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 __init__(self, name):
self.name = name
def update(self, message):
print(f"{self.name} received: {message}")
Use cases: Event systems, UI updates, publish-subscribe systems, notifications.
Problem: Select algorithm at runtime.
class SortStrategy:
def sort(self, data):
pass
class Sorter:
def __init__(self, strategy):
self.strategy = strategy
def sort_data(self, data):
return self.strategy.sort(data)
sorter = Sorter(BubbleSort())
sorter.sort_data(data)
Use cases: Payment methods, compression algorithms, route finding.
You started wondering how to model the real world in code.
Now you understand.
Classes define blueprints—structure and behavior. Objects are instances with their own data.
Constructors initialize objects, setting up initial state when created.
Static members belong to the class itself, shared by all instances. Instance members belong to individual objects.
Encapsulation bundles data and methods, hiding internal details and protecting integrity through access control.
Inheritance creates hierarchies—child classes inherit from parents, promoting code reuse and logical relationships.
Polymorphism allows different objects to respond to the same interface in their own way—one method call, many behaviors.
Abstract classes define contracts that subclasses must fulfill, preventing incomplete instantiation.
Composition (strong ownership) and aggregation (weak association) model "has-a" relationships with different lifetimes.
Design patterns provide proven solutions—Singleton for single instances, Factory for object creation, Observer for notifications, Strategy for interchangeable algorithms.
Every modern application uses OOP—games with player objects, banking systems with account objects, websites with user objects, operating systems with process objects.
Understanding OOP changes how you think about software design. You're no longer writing scattered functions and variables. You're creating organized systems that model reality.