Who this guide is for
- Learners who already understand Python fundamentals and functions
- Developers who want to model real-world entities with clean structure
- Anyone preparing to build larger applications with reusable components
What you’ll learn
- How classes and objects work in Python
- The role of
__init__and different method types (instance, class, static) - How inheritance, encapsulation, and polymorphism improve design
- When and why to use abstract base classes (
abc) - Common OOP mistakes and how to avoid overengineering
Why this topic matters
As projects grow, plain scripts become hard to maintain. OOP helps you organize data and behavior into clear units (classes) so your code is easier to read, test, and extend.
In Python, OOP is practical rather than rigid. You can combine object-oriented design with functional patterns, then choose the simplest structure that fits your problem. Learning OOP well gives you a strong foundation for frameworks, APIs, and scalable codebases.
Core concepts
Classes, objects, and constructors
A class is a blueprint; an object is an instance of that blueprint.
class User:
def __init__(self, username, email):
self.username = username
self.email = email
def profile(self):
return f"{self.username} <{self.email}>"
user = User("katie", "[email protected]")
print(user.profile())
Expected output:
katie <[email protected]>
__init__ runs when a new object is created. It initializes object state.
Method types and encapsulation
Python classes commonly use three method types:
- Instance methods: operate on object data (
self) - Class methods: operate on class-level context (
cls) - Static methods: utility logic related to class domain but no instance/class state
class Temperature:
scale = "Celsius"
def __init__(self, celsius):
self._celsius = celsius
def to_fahrenheit(self):
return (self._celsius * 9 / 5) + 32
@classmethod
def from_fahrenheit(cls, fahrenheit):
celsius = (fahrenheit - 32) * 5 / 9
return cls(celsius)
@staticmethod
def is_valid(value):
return isinstance(value, (int, float))
The _celsius naming style signals internal attribute intent (convention-based encapsulation).
Encapsulation visibility conventions in Python:
- Public attribute:
name(intended for normal external access) - Protected-style attribute:
_balance(convention: internal use) - Private-style attribute:
__pin(name-mangled to reduce accidental access)
class Wallet:
def __init__(self, owner, pin):
self.owner = owner # public
self._balance = 0 # protected-style
self.__pin = pin # private-style
def deposit(self, amount):
self._balance += amount
def check_pin(self, pin):
return self.__pin == pin
w = Wallet("Ava", "1234")
w.deposit(100)
print(w.owner) # public access
print(w._balance) # possible, but convention says internal
print(w.check_pin("1234"))
Inheritance, polymorphism, and abstract base classes
Inheritance allows child classes to reuse and extend parent behavior. Polymorphism allows different classes to share a common interface.
Common inheritance forms:
- Single inheritance: one child inherits one parent
- Multiple inheritance: one child inherits multiple parents
- Multilevel inheritance: class chain (Grandparent -> Parent -> Child)
Inheritance type examples:
# Single inheritance
class Animal:
def speak(self):
return "..."
class Dog(Animal):
def speak(self):
return "Woof"
# Multiple inheritance
class FlyMixin:
def fly(self):
return "Flying"
class SwimMixin:
def swim(self):
return "Swimming"
class Duck(FlyMixin, SwimMixin):
pass
# Multilevel inheritance
class Vehicle:
def category(self):
return "Transport"
class Car(Vehicle):
def wheels(self):
return 4
class ElectricCar(Car):
def power_source(self):
return "Battery"
print(Dog().speak())
print(Duck().fly(), Duck().swim())
print(ElectricCar().category(), ElectricCar().wheels(), ElectricCar().power_source())
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, amount):
pass
class CardPayment(PaymentProcessor):
def pay(self, amount):
return f"Paid ${amount} by card"
class WalletPayment(PaymentProcessor):
def pay(self, amount):
return f"Paid ${amount} by wallet"
def checkout(processor, amount):
print(processor.pay(amount))
checkout(CardPayment(), 120)
checkout(WalletPayment(), 85)
Expected output:
Paid $120 by card
Paid $85 by wallet
This pattern keeps extension easy while preserving a consistent API.
Simple UML-style sketch (text-based):
PaymentProcessor (abstract)
├── CardPayment
└── WalletPayment
This kind of class diagram helps document relationships for teammates before implementation.
Step-by-step walkthrough
Step 1 — Create a simple class model
Start with one class that owns both data and behavior.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Insufficient balance")
self.balance -= amount
This gives you a clean unit with clear responsibilities.
Step 2 — Add inheritance for specialized behavior
Create a subclass for savings accounts with extra rules.
class SavingsAccount(BankAccount):
def __init__(self, owner, balance=0, interest_rate=0.03):
super().__init__(owner, balance)
self.interest_rate = interest_rate
def apply_interest(self):
self.balance += self.balance * self.interest_rate
Use super() to reuse parent initialization cleanly.
Step 3 — Introduce polymorphic interfaces
Design code that works with any class implementing the same method.
class EmailNotifier:
def send(self, message):
print(f"Email: {message}")
class SMSNotifier:
def send(self, message):
print(f"SMS: {message}")
def notify_all(notifiers, message):
for notifier in notifiers:
notifier.send(message)
notify_all([EmailNotifier(), SMSNotifier()], "Payment received")
This keeps business logic flexible without if/elif chains for every type.
Practical examples
Example 1 — Product inventory model
Modeling products as objects makes state updates clearer.
class Product:
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
def sell(self, quantity):
if quantity > self.stock:
raise ValueError("Not enough stock")
self.stock -= quantity
def __str__(self):
return f"{self.name} | ${self.price} | stock={self.stock}"
laptop = Product("Laptop", 999, 5)
laptop.sell(2)
print(laptop)
Expected output:
Laptop | $999 | stock=3
Example 2 — Abstract report exporters
Use abstract base classes to enforce required methods.
from abc import ABC, abstractmethod
class ReportExporter(ABC):
@abstractmethod
def export(self, data):
pass
class CSVExporter(ReportExporter):
def export(self, data):
return f"CSV export: {data}"
class JSONExporter(ReportExporter):
def export(self, data):
return f"JSON export: {data}"
for exporter in [CSVExporter(), JSONExporter()]:
print(exporter.export({"users": 120}))
Expected output:
CSV export: {'users': 120}
JSON export: {'users': 120}
This pattern is useful when you support multiple output formats or backends.
Common mistakes and how to avoid them
- Turning every script into a class too early -> Start simple; introduce classes when state + behavior naturally belong together.
- Exposing mutable internals carelessly -> Use clear methods (
deposit,withdraw) instead of uncontrolled direct changes. - Deep inheritance chains -> Prefer composition when inheritance becomes hard to reason about.
- Ignoring interface consistency -> Keep method names/parameters predictable across related classes.
- Forgetting single responsibility -> One class should do one core job well.
Quick practice
- Build a
Studentclass with attributes (name,scores) and methods (add_score,average_score). - Create
Animalparent class with subclassesDogandCat, each implementing aspeak()method. - Define an abstract
Storageclass and implementLocalStorage+CloudStoragewith a sharedsave(data)interface.
Key takeaways
- OOP helps structure larger Python programs through reusable, testable components.
__init__, method types, and encapsulation conventions are core daily tools.- Inheritance and polymorphism improve extensibility when used with restraint.
- Abstract base classes help enforce contracts in multi-implementation designs.
Next step
Continue to Data Structures & Algorithms Basics. In the next guide, you will review core structures, complexity thinking, and essential sorting/searching patterns used in interviews and real systems.
No Comments