What Is Object-Oriented Programming?
Object-Oriented Programming (OOP) is a programming paradigm that organizes code around objects rather than functions and logic. An object combines data (attributes) and behavior (methods) into a single unit. OOP helps you write code that is modular, reusable, and easier to maintain as projects grow in complexity.
Python is an object-oriented language at its core. Even basic types like integers, strings, and lists are objects. Understanding OOP is essential for writing professional Python code, working with frameworks like Django, and collaborating on large codebases.
Classes and Objects
A class is a blueprint for creating objects. An object is a specific instance of a class. Think of a class as a cookie cutter and objects as the cookies it produces.
Defining Your First Class
class Dog:
# Class attribute (shared by all instances)
species = "Canis familiaris"
# Constructor (initializer)
def __init__(self, name, age, breed):
# Instance attributes (unique to each instance)
self.name = name
self.age = age
self.breed = breed
# Instance method
def bark(self):
return f"{self.name} says: Woof!"
# Instance method
def description(self):
return f"{self.name} is a {self.age}-year-old {self.breed}"
# String representation
def __str__(self):
return self.description()
# Creating objects (instances)
rex = Dog("Rex", 5, "German Shepherd")
bella = Dog("Bella", 3, "Golden Retriever")
print(rex.bark()) # "Rex says: Woof!"
print(bella.description()) # "Bella is a 3-year-old Golden Retriever"
print(rex.species) # "Canis familiaris"
Understanding self
The self parameter refers to the current instance of the class. It is how an object accesses its own attributes and methods. Python passes self automatically when you call a method on an object, so you never need to pass it explicitly.
Class vs Instance Attributes
class Counter:
# Class attribute - shared by ALL instances
total_counters = 0
def __init__(self, name):
# Instance attribute - unique to each instance
self.name = name
self.count = 0
Counter.total_counters += 1
def increment(self):
self.count += 1
def __repr__(self):
return f"Counter('{self.name}', count={self.count})"
c1 = Counter("page_views")
c2 = Counter("clicks")
c1.increment()
c1.increment()
c2.increment()
print(c1) # Counter('page_views', count=2)
print(c2) # Counter('clicks', count=1)
print(Counter.total_counters) # 2
The Four Pillars of OOP
1. Encapsulation
Encapsulation is the bundling of data and methods that operate on that data within a single class, and restricting direct access to some of the object's components. In Python, we use naming conventions to indicate access levels.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner # Public attribute
self._account_type = "savings" # Protected (convention)
self.__balance = balance # Private (name mangling)
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return f"Deposited ${amount}. New balance: ${self.__balance}"
return "Deposit amount must be positive"
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
return f"Withdrew ${amount}. New balance: ${self.__balance}"
return "Invalid withdrawal amount"
def get_balance(self):
return self.__balance
@property
def balance(self):
return self.__balance
account = BankAccount("Alice", 1000)
account.deposit(500)
print(account.balance) # 1500 (using property)
# account.__balance # AttributeError! Cannot access directly
2. Inheritance
Inheritance allows a class to inherit attributes and methods from a parent class. This promotes code reuse and establishes a natural hierarchy between classes.
class Animal:
def __init__(self, name, sound):
self.name = name
self.sound = sound
def speak(self):
return f"{self.name} says {self.sound}!"
def __str__(self):
return f"Animal: {self.name}"
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name, "Woof") # Call parent constructor
self.breed = breed
def fetch(self, item):
return f"{self.name} fetches the {item}!"
class Cat(Animal):
def __init__(self, name, indoor=True):
super().__init__(name, "Meow")
self.indoor = indoor
def purr(self):
return f"{self.name} purrs contentedly"
dog = Dog("Rex", "German Shepherd")
cat = Cat("Whiskers")
print(dog.speak()) # "Rex says Woof!"
print(dog.fetch("ball")) # "Rex fetches the ball!"
print(cat.speak()) # "Whiskers says Meow!"
print(cat.purr()) # "Whiskers purrs contentedly"
# isinstance checks
print(isinstance(dog, Dog)) # True
print(isinstance(dog, Animal)) # True
3. Polymorphism
Polymorphism means that different classes can be used through the same interface. Objects of different types can respond to the same method call in their own way.
class Shape:
def area(self):
raise NotImplementedError("Subclasses must implement area()")
def perimeter(self):
raise NotImplementedError("Subclasses must implement perimeter()")
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
def perimeter(self):
import math
return 2 * math.pi * self.radius
# Polymorphism in action
shapes = [Rectangle(5, 3), Circle(4), Rectangle(10, 2)]
for shape in shapes:
# Same method call, different behavior
print(f"Area: {shape.area():.2f}, Perimeter: {shape.perimeter():.2f}")
4. Abstraction
Abstraction hides complex implementation details and shows only the necessary features. Python supports abstraction through abstract base classes.
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def connect(self):
pass
@abstractmethod
def execute(self, query):
pass
def log(self, message):
print(f"[DB LOG] {message}")
class PostgresDB(Database):
def connect(self):
self.log("Connecting to PostgreSQL...")
return "Connected to PostgreSQL"
def execute(self, query):
self.log(f"Executing: {query}")
return f"PostgreSQL result for: {query}"
class MongoDB(Database):
def connect(self):
self.log("Connecting to MongoDB...")
return "Connected to MongoDB"
def execute(self, query):
self.log(f"Executing: {query}")
return f"MongoDB result for: {query}"
# db = Database() # TypeError! Cannot instantiate abstract class
pg = PostgresDB()
pg.connect()
pg.execute("SELECT * FROM users")
Advanced OOP Concepts
Class Methods and Static Methods
class Employee:
raise_percentage = 1.05 # 5% raise
def __init__(self, name, salary):
self.name = name
self.salary = salary
# Regular method - operates on instance
def apply_raise(self):
self.salary *= self.raise_percentage
# Class method - operates on class
@classmethod
def set_raise_percentage(cls, percentage):
cls.raise_percentage = percentage
# Class method as alternative constructor
@classmethod
def from_string(cls, emp_string):
name, salary = emp_string.split("-")
return cls(name, int(salary))
# Static method - no access to instance or class
@staticmethod
def is_workday(day):
return day.weekday() < 5
emp = Employee.from_string("Alice-75000")
print(emp.name, emp.salary) # Alice 75000
Dunder (Magic) Methods
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __len__(self):
return int((self.x ** 2 + self.y ** 2) ** 0.5)
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2) # Vector(4, 6)
print(v1 - v2) # Vector(2, 2)
print(v1 * 3) # Vector(9, 12)
print(len(v1)) # 5
OOP Best Practices
- Single Responsibility Principle - Each class should have one reason to change. Do not create god classes that do everything.
- Favor Composition Over Inheritance - Use inheritance for "is-a" relationships and composition for "has-a" relationships.
- Keep It Simple - Do not use OOP just for the sake of it. Simple scripts do not need classes.
- Use Properties - Use @property instead of getter/setter methods for cleaner APIs.
- Write Docstrings - Document your classes and methods so others (and future you) can understand them.
Object-oriented programming is a powerful tool in your Python toolkit. Master these concepts and you will be able to build complex, maintainable applications that scale with your needs.