Python OOP — Deep Dive with Examples
Object-Oriented Programming (OOP) in Python lets you model real-world entities as classes with attributes and methods. Instead of scattering data and logic across variables and functions, you bundle them together into reusable blueprints.
What You’ll Learn
- How to define classes and create objects
- Inheritance, polymorphism, and encapsulation explained step by step
@property, magic methods (__str__,__repr__,__len__), and dataclasses- How to build a complete
BankAccountclass as a real-world example
Why OOP Matters
Imagine building Durga Antivirus Pro without OOP — every scan would need separate lists of signatures, separate functions for file inspection, and manual tracking of state. With OOP, a Scanner class holds its own signature database, scan state, and methods. DodaZIP models archives, files, and compression profiles as objects. OOP makes complex software organized, testable, and extensible.
flowchart LR
A["Python Basics"] --> B["Functions"]
B --> C["OOP"]
C --> D["Decorators"]
D --> E["Generators"]
E --> F["Async"]
A:::done --> B:::done --> C:::current
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#2563eb,stroke:#2563eb,color:#fff
style C fill:#2563eb,stroke:#2563eb,color:#fff
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#dbeafe,stroke:#2563eb,color:#1e40af
style F fill:#f1f5f9,stroke:#94a3b8,color:#64748b
What is a Class?
A class is a blueprint. An object is an instance built from that blueprint. Think of a class as a cookie cutter and objects as the cookies themselves.
class Dog:
def __init__(self, name: str, breed: str):
self.name = name
self.breed = breed
def bark(self) -> str:
return f"{self.name} says Woof!"
# Create objects (instances)
fido = Dog("Fido", "Golden Retriever")
rex = Dog("Rex", "German Shepherd")
print(fido.bark()) # Fido says Woof!
print(rex.bark()) # Rex says Woof!__init__is the constructor — Python calls it automatically when you create an objectselfrefers to the current instance — every method receives it as the first argumentself.nameandself.breedare instance attributes — unique to each object
The Four Pillars of OOP
1. Encapsulation
Encapsulation means keeping data and the methods that operate on it together, and hiding internal details. In Python, prefix an attribute with _ (convention for protected) or __ (name-mangled for private):
class BankAccount:
def __init__(self, owner: str, initial_balance: float = 0):
self.owner = owner
self.__balance = initial_balance # private attribute
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.__balance += amount
def withdraw(self, amount: float) -> None:
if amount > self.__balance:
raise ValueError("Insufficient funds")
self.__balance -= amount
def get_balance(self) -> float:
return self.__balance
acc = BankAccount("Alice", 1000)
acc.deposit(500)
acc.withdraw(200)
print(acc.get_balance()) # 1300
# print(acc.__balance) # AttributeError!The __balance attribute is name-mangled to _BankAccount__balance — still accessible if you know the trick, but the leading __ signals “internal, don’t touch.”
2. Inheritance
A child class inherits attributes and methods from a parent class, then adds or overrides what it needs:
class Animal:
def __init__(self, name: str):
self.name = name
def speak(self) -> str:
return f"{self.name} makes a sound"
class Cat(Animal):
def speak(self) -> str:
return f"{self.name} says Meow!"
class Dog(Animal):
def speak(self) -> str:
return f"{self.name} says Woof!"
animals = [Cat("Whiskers"), Dog("Buddy")]
for a in animals:
print(a.speak())
# Whiskers says Meow!
# Buddy says Woof!3. Polymorphism
Polymorphism means “many forms” — the same interface works with different types. The speak() method above is polymorphic: each animal class provides its own implementation, and the calling code doesn’t care which subclass it’s dealing with.
def make_animal_speak(animal: Animal) -> None:
print(animal.speak())
make_animal_speak(Cat("Mittens")) # Mittens says Meow!
make_animal_speak(Dog("Rover")) # Rover says Woof!4. Abstraction
Abstraction means exposing only essential details and hiding complexity. Python achieves this through abstract base classes (ABCs):
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
# s = Shape() # TypeError — can't instantiate abstract class
c = Circle(5)
print(c.area()) # 78.53975@property — Controlled Attribute Access
The @property decorator lets you define methods that look like attributes. Use it for computed values or to add validation:
class Temperature:
def __init__(self, celsius: float):
self._celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value
@property
def fahrenheit(self) -> float:
return self._celsius * 9/5 + 32
t = Temperature(25)
print(t.fahrenheit) # 77.0
t.celsius = 30
print(t.fahrenheit) # 86.0
# t.celsius = -300 # ValueError!Magic Methods (Dunder Methods)
Magic methods start and end with __ and let your objects work with Python’s built-in operations:
class Book:
def __init__(self, title: str, author: str, pages: int):
self.title = title
self.author = author
self.pages = pages
def __str__(self) -> str:
return f"'{self.title}' by {self.author}"
def __repr__(self) -> str:
return f"Book('{self.title}', '{self.author}', {self.pages})"
def __len__(self) -> int:
return self.pages
def __eq__(self, other: object) -> bool:
if not isinstance(other, Book):
return NotImplemented
return self.title == other.title and self.author == other.author
book = Book("1984", "George Orwell", 328)
print(str(book)) # '1984' by George Orwell
print(repr(book)) # Book('1984', 'George Orwell', 328)
print(len(book)) # 328
print(book == Book("1984", "George Orwell", 328)) # TrueDataclasses (Python 3.7+)
The dataclass decorator auto-generates __init__, __repr__, __eq__, and more:
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
email: str = ""
def is_adult(self) -> bool:
return self.age >= 18
p = Person("Alice", 30, "alice@example.com")
print(p) # Person(name='Alice', age=30, email='alice@example.com')
print(p.is_adult()) # TrueCommon Mistakes
1. Forgetting self in Method Definitions
class MyClass:
def method(): # Missing self!
return "hello"
# obj.method() # TypeError!Fix: Always include self as the first parameter of instance methods.
2. Using Mutable Defaults in __init__
class Student:
def __init__(self, grades=[]): # Shared list!
self.grades = gradesFix: Use None and create a new list in the body.
3. Confusing @staticmethod and @classmethod
@staticmethod— no access toselforcls(like a plain function inside the class)@classmethod— receivescls(the class itself), useful for alternative constructors
4. Not Calling super().__init__() in Subclasses
class Parent:
def __init__(self):
self.value = 42
class Child(Parent):
def __init__(self):
super().__init__() # Required!
self.extra = "data"5. Overusing Inheritance
Favor composition over inheritance. A Car has a Engine (composition) rather than Car is a Engine (inheritance).
Practice Questions
1. What’s the output?
class A:
def greet(self):
return "Hello from A"
class B(A):
def greet(self):
return "Hello from B"
obj = B()
print(obj.greet())"Hello from B" — method override.
2. Why does print(acc.__balance) fail after the BankAccount example?
Because __balance is name-mangled to _BankAccount__balance. It’s a private convention.
3. Convert this class to a dataclass:
class Point:
def __init__(self, x, y):
self.x = x
self.y = yfrom dataclasses import dataclass
@dataclass
class Point:
x: float
y: float4. What’s the difference between @property and a regular method?@property lets you access computed values like attributes (no () call), and supports .setter for validation.
Challenge: Build a Library class that stores a list of Book objects. Implement add_book, find_by_author, and __len__ (returns total books).
Solution
@dataclass
class Book:
title: str
author: str
pages: int
class Library:
def __init__(self):
self._books: list[Book] = []
def add_book(self, book: Book) -> None:
self._books.append(book)
def find_by_author(self, author: str) -> list[Book]:
return [b for b in self._books if b.author.lower() == author.lower()]
def __len__(self) -> int:
return len(self._books)Mini Project: BankAccount Class
Build a full bank account system:
class BankAccount:
"""A full-featured bank account."""
_account_counter = 0 # class attribute — shared across all instances
def __init__(self, owner: str, initial_balance: float = 0):
BankAccount._account_counter += 1
self.account_number = BankAccount._account_counter
self.owner = owner
self.__balance = initial_balance
self.__transactions: list[str] = []
def deposit(self, amount: float) -> str:
if amount <= 0:
raise ValueError("Amount must be positive")
self.__balance += amount
self.__transactions.append(f"Deposit: +${amount:.2f}")
return f"Deposited ${amount:.2f}. Balance: ${self.__balance:.2f}"
def withdraw(self, amount: float) -> str:
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > self.__balance:
raise ValueError("Insufficient funds")
self.__balance -= amount
self.__transactions.append(f"Withdrawal: -${amount:.2f}")
return f"Withdrew ${amount:.2f}. Balance: ${self.__balance:.2f}"
@property
def balance(self) -> float:
return self.__balance
def statement(self) -> list[str]:
return self.__transactions.copy()
def __str__(self) -> str:
return f"Account #{self.account_number} ({self.owner}): ${self.__balance:.2f}"
def __repr__(self) -> str:
return f"BankAccount('{self.owner}', {self.__balance})"
# Usage
acc1 = BankAccount("Alice", 1000)
print(acc1.deposit(500))
print(acc1.withdraw(200))
print(acc1)
print(acc1.statement())Expected output:
Deposited $500.00. Balance: $1500.00
Withdrew $200.00. Balance: $1300.00
Account #1 (Alice): $1300.00
['Deposit: +$500.00', 'Withdrawal: -$200.00']What’s Next
Now that you understand OOP, explore decorators and generators — they build on the same function-as-object concepts.
| Topic | Description | Link |
|---|---|---|
| Python Decorators | Extend functions with @ syntax | https://tutorials.dodatech.com/programming-languages/python/py-decorators/ |
| Python Generators | Lazy iteration with yield | https://tutorials.dodatech.com/programming-languages/python/py-generators/ |
| OOP glossary | OOP terminology reference | OOP |
Practice tip: Extend BankAccount with transfer_to(other_account, amount) and interest calculation. The best way to learn OOP is to model something you understand — start with your daily life objects.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro