Python OOP Guide: Classes, Inheritance, Magic Methods and Design Patterns
Python supports multiple programming paradigms, but object-oriented programming is central to writing clean, reusable, and maintainable Python code. Whether you are building a web framework, a data pipeline, or a CLI tool, understanding Python OOP deeply sets you apart.
Classes and Instances
pythonclass Dog: # Class variable -- shared across all instances species = "Canis familiaris" def __init__(self, name, age): # Instance variables -- unique to each instance self.name = name self.age = age def bark(self): return f"{self.name} says: Woof!" def __repr__(self): return f"Dog(name={self.name!r}, age={self.age!r})" def __str__(self): return f"{self.name}, {self.age} years old" dog1 = Dog("Rex", 3) dog2 = Dog("Bella", 5) print(dog1.bark()) # Rex says: Woof! print(dog1.species) # Canis familiaris print(Dog.species) # same -- class variable print(repr(dog1)) # Dog(name='Rex', age=3) print(str(dog1)) # Rex, 3 years old
Magic Methods (Dunder Methods)
Methods with double underscores on both sides are called dunder (double underscore) methods. They define how objects behave with Python's built-in operations.
pythonclass 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 __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 2 def __getitem__(self, index): return (self.x, self.y)[index] def __abs__(self): return (self.x ** 2 + self.y ** 2) ** 0.5 v1 = Vector(1, 2) v2 = Vector(3, 4) print(v1 + v2) # Vector(4, 6) print(v1 * 3) # Vector(3, 6) print(v1 == v1) # True print(len(v1)) # 2 print(v1[0]) # 1 print(abs(v2)) # 5.0
Key dunder methods
| Method | Triggered by |
|---|---|
__init__ | obj = Class() |
__repr__ | repr(obj), REPL display |
__str__ | str(obj), print(obj) |
__len__ | len(obj) |
__eq__ | obj == other |
__lt__ | obj < other |
__add__ | obj + other |
__getitem__ | obj[key] |
__contains__ | item in obj |
__iter__ | for item in obj |
__enter__, __exit__ | with obj: |
Properties
Use @property to control attribute access with getter/setter logic:
pythonclass Temperature: def __init__(self, celsius): self._celsius = celsius @property def celsius(self): return self._celsius @celsius.setter def celsius(self, value): if value < -273.15: raise ValueError("Temperature below absolute zero") self._celsius = value @property def fahrenheit(self): return self._celsius * 9/5 + 32 @fahrenheit.setter def fahrenheit(self, value): self.celsius = (value - 32) * 5/9 t = Temperature(25) print(t.celsius) # 25 print(t.fahrenheit) # 77.0 t.fahrenheit = 32 print(t.celsius) # 0.0 t.celsius = -300 # ValueError: Temperature below absolute zero
Class Methods and Static Methods
pythonclass Date: def __init__(self, year, month, day): self.year = year self.month = month self.day = day @classmethod def from_string(cls, date_string): year, month, day = map(int, date_string.split("-")) return cls(year, month, day) @classmethod def today(cls): import datetime d = datetime.date.today() return cls(d.year, d.month, d.day) @staticmethod def is_valid(year, month, day): return 1 <= month <= 12 and 1 <= day <= 31 def __repr__(self): return f"Date({self.year}, {self.month}, {self.day})" d = Date.from_string("2025-03-11") # classmethod factory today = Date.today() print(Date.is_valid(2025, 13, 1)) # False -- staticmethod
@classmethodreceivescls(the class) as first argument β used for factory methods and alternative constructors@staticmethodreceives nothing extra β just a function namespaced inside the class
Inheritance
pythonclass Animal: def __init__(self, name): self.name = name def speak(self): raise NotImplementedError("Subclass must implement speak()") def __repr__(self): return f"{self.__class__.__name__}({self.name!r})" class Dog(Animal): def speak(self): return f"{self.name}: Woof!" class Cat(Animal): def speak(self): return f"{self.name}: Meow!" class Duck(Animal): def speak(self): return f"{self.name}: Quack!" animals = [Dog("Rex"), Cat("Whiskers"), Duck("Donald")] for animal in animals: print(animal.speak())
super()
Call the parent class's method:
pythonclass Vehicle: def __init__(self, make, model): self.make = make self.model = model def describe(self): return f"{self.make} {self.model}" class ElectricCar(Vehicle): def __init__(self, make, model, battery_kwh): super().__init__(make, model) # call parent __init__ self.battery_kwh = battery_kwh def describe(self): base = super().describe() # call parent describe() return f"{base} (Electric, {self.battery_kwh}kWh)" car = ElectricCar("Tesla", "Model 3", 82) print(car.describe()) # Tesla Model 3 (Electric, 82kWh)
Multiple Inheritance and MRO
Python supports multiple inheritance. The Method Resolution Order (MRO) determines which method is called when the same name exists in multiple bases:
pythonclass A: def hello(self): return "A" class B(A): def hello(self): return "B" class C(A): def hello(self): return "C" class D(B, C): pass d = D() print(d.hello()) # "B" -- follows MRO: D -> B -> C -> A print(D.__mro__) # (D, B, C, A, object)
Python uses C3 linearization to compute MRO. You can always inspect it with ClassName.__mro__.
Abstract Classes
Use abc.ABC to define abstract base classes that enforce method implementation:
pythonfrom abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self) -> float: pass @abstractmethod def perimeter(self) -> float: pass def describe(self): return f"Area: {self.area():.2f}, Perimeter: {self.perimeter():.2f}" 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 shape = Shape() # TypeError: Can't instantiate abstract class circle = Circle(5) print(circle.describe()) # Area: 78.54, Perimeter: 31.42
Dataclasses
Python 3.7+ dataclasses reduce boilerplate for data-holding classes:
pythonfrom dataclasses import dataclass, field @dataclass class Product: name: str price: float category: str = "general" tags: list = field(default_factory=list) def discounted_price(self, discount_pct): return self.price * (1 - discount_pct / 100) p = Product("Laptop", 999.99, "electronics", ["tech", "portable"]) print(p) # Product(name='Laptop', price=999.99, ...) print(p == Product("Laptop", 999.99, "electronics", ["tech", "portable"])) # True @dataclass(frozen=True) # immutable class Point: x: float y: float
Dataclasses auto-generate __init__, __repr__, and __eq__. Add frozen=True for immutability, order=True for comparison operators.
Common Interview Questions
Q: What is the difference between __str__ and __repr__?
__repr__ should return an unambiguous representation useful for debugging β ideally something you could paste into the interpreter to recreate the object. __str__ should return a human-readable string. print() uses __str__; the REPL and repr() use __repr__. If only __repr__ is defined, it is used as fallback for __str__.
Q: What is the difference between a classmethod and a staticmethod?
A classmethod receives the class as its first argument (cls) and can access or modify class state. A staticmethod receives no implicit first argument β it is just a regular function that lives in the class namespace for organizational purposes.
Q: What does super() do in Python?
super() returns a proxy object that delegates method calls to the next class in the MRO. It is most commonly used to call the parent class's __init__ or an overridden method while still executing the subclass version.
Practice Python on Froquiz
OOP is tested in Python interviews at every level. Test your Python knowledge on Froquiz β covering OOP, decorators, comprehensions, async, and more.
Summary
__init__initializes instances; class variables are shared, instance variables are unique- Dunder methods define behavior for built-in operations (
+,len,in,with, etc.) @propertyadds getter/setter logic to attribute access@classmethodreceivesclsβ use for factory methods;@staticmethodis a plain namespaced functionsuper()delegates to the next class in the MRO- Multiple inheritance uses C3 linearization β inspect with
ClassName.__mro__ abc.ABCand@abstractmethodenforce interface contracts@dataclasseliminates boilerplate for data-holding classes