Skip to content
Python OOP — Deep Dive with Examples

Python OOP — Deep Dive with Examples

DodaTech Updated Jun 15, 2026 7 min read

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 BankAccount class 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
  
Prerequisite: You should understand Python functions, lists, and dicts. If those are fuzzy, review https://tutorials.dodatech.com/programming-languages/python/py-functions/ and https://tutorials.dodatech.com/programming-languages/python/py-lists-dicts/ first.

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 object
  • self refers to the current instance — every method receives it as the first argument
  • self.name and self.breed are 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))  # True

Dataclasses (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())     # True

Common 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 = grades

Fix: Use None and create a new list in the body.

3. Confusing @staticmethod and @classmethod

  • @staticmethod — no access to self or cls (like a plain function inside the class)
  • @classmethod — receives cls (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 = y
from dataclasses import dataclass
@dataclass
class Point:
    x: float
    y: float

4. 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.

TopicDescriptionLink
Python DecoratorsExtend functions with @ syntaxhttps://tutorials.dodatech.com/programming-languages/python/py-decorators/
Python GeneratorsLazy iteration with yieldhttps://tutorials.dodatech.com/programming-languages/python/py-generators/
OOP glossaryOOP terminology referenceOOP

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