Python’s OOP is fundamentally different from Java or C++. There are no access modifiers (no private, protected, public). There are no interfaces. There’s no method overloading. Instead, Python uses duck typing, dunder methods, and conventions.
Classes — The Basics
class User:
"""A user in the system."""
# Class variable — shared by all instances
default_role = "viewer"
def __init__(self, name: str, email: str):
# Instance variables — unique to each instance
self.name = name
self.email = email
self.role = User.default_role
def promote(self, new_role: str):
self.role = new_role
def __repr__(self):
return f"User(name={self.name!r}, role={self.role!r})"
alice = User("Alice", "[email protected]")
alice.promote("admin")
print(alice) # User(name='Alice', role='admin')self Explained
self is not a keyword — it’s a convention. It’s the first argument to every instance method and refers to the current instance. It’s equivalent to this in Java/JS, but explicit.
class Dog:
def bark(self):
return f"{self.name} says woof!"
# When you call d.bark(), Python translates it to Dog.bark(d)
d = Dog()
d.name = "Rex"
d.bark() # "Rex says woof!"
Dog.bark(d) # Same thing — self is just dDunder (Magic) Methods
Dunder methods let your class work with Python’s built-in operations.
class Vector:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
# String representation
def __repr__(self):
return f"Vector({self.x}, {self.y})"
# Equality
def __eq__(self, other):
return self.x == other.x and self.y == other.y
# Hashing (needed if you want to use as dict key or in set)
def __hash__(self):
return hash((self.x, self.y))
# Arithmetic operators
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)
# Absolute value (magnitude)
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
# Boolean (is it a zero vector?)
def __bool__(self):
return abs(self) > 0
# Make it iterable (for unpacking)
def __iter__(self):
yield self.x
yield self.y
# Now the class works naturally with Python
v1 = Vector(3, 4)
v2 = Vector(1, 2)
v1 + v2 # Vector(4, 6)
v1 * 3 # Vector(9, 12)
abs(v1) # 5.0
bool(Vector(0, 0)) # False
x, y = v1 # x=3, y=4 (unpacking via __iter__)Inheritance
class Animal:
def __init__(self, name: str):
self.name = name
def speak(self):
raise NotImplementedError("Subclasses must implement speak()")
class Dog(Animal):
def speak(self):
return f"{self.name} says woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says meow!"
# Polymorphism — no interface declaration needed
animals = [Dog("Rex"), Cat("Whiskers"), Dog("Buddy")]
for animal in animals:
print(animal.speak())
# Rex says woof!
# Whiskers says meow!
# Buddy says woof!super() — Calling Parent Methods
class Employee:
def __init__(self, name: str, salary: float):
self.name = name
self.salary = salary
class Manager(Employee):
def __init__(self, name: str, salary: float, team_size: int):
super().__init__(name, salary) # Call parent __init__
self.team_size = team_size
m = Manager("Alice", 150000, 8)
m.name # "Alice" (inherited)
m.team_size # 8Multiple Inheritance and MRO
class A:
def greet(self):
return "A"
class B(A):
def greet(self):
return "B"
class C(A):
def greet(self):
return "C"
class D(B, C):
pass
d = D()
d.greet() # "B" — follows MRO
D.__mro__ # (D, B, C, A, object)
# super() follows MRO, not just "parent"
class B(A):
def greet(self):
next_greet = super().greet() # Calls C.greet(), not A.greet()!
return f"B -> {next_greet}"Privacy Convention
Python has no private keyword. Instead, it uses naming conventions:
class Account:
def __init__(self, balance):
self.balance = balance # Public
self._internal_id = "abc123" # "Private by convention" — don't touch from outside
self.__secret = "hidden" # Name-mangled to _Account__secret
a = Account(100)
a.balance # 100 (public, fine)
a._internal_id # "abc123" (works, but you shouldn't)
# a.__secret # AttributeError
a._Account__secret # "hidden" (name mangling — you CAN access it, but don't)The Python philosophy: “We’re all consenting adults.” A single underscore _prefix means “this is internal” and that’s enough.
Dataclasses (Python 3.7+)
Dataclasses eliminate the boilerplate of __init__, __repr__, __eq__, etc.
from dataclasses import dataclass, field
@dataclass
class Product:
name: str
price: float
quantity: int = 0
tags: list[str] = field(default_factory=list)
# Auto-generated: __init__, __repr__, __eq__
p = Product("Widget", 9.99, 100, ["sale", "new"])
print(p) # Product(name='Widget', price=9.99, quantity=100, tags=['sale', 'new'])
p2 = Product("Widget", 9.99, 100, ["sale", "new"])
p == p2 # True (auto __eq__)
# Immutable dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
pt = Point(3, 4)
# pt.x = 5 # FrozenInstanceError!
# Post-init processing
@dataclass
class Order:
items: list[str]
subtotal: float
tax_rate: float = 0.08
total: float = field(init=False) # Computed, not passed in
def __post_init__(self):
self.total = self.subtotal * (1 + self.tax_rate)
order = Order(["Widget"], 100.0)
order.total # 108.0Abstract Base Classes
When you do need interface-like contracts:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
@abstractmethod
def perimeter(self) -> float:
...
# shape = Shape() # TypeError: Can't instantiate abstract class
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)Protocols (Structural Typing — Python 3.8+)
Protocols are Python’s answer to “duck typing with type checking”:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> str: ...
class Circle:
def draw(self) -> str:
return "Drawing circle"
class Square:
def draw(self) -> str:
return "Drawing square"
def render(shape: Drawable) -> None:
"""Accepts any object with a draw() method — no inheritance needed."""
print(shape.draw())
render(Circle()) # Works — Circle has draw()
render(Square()) # Works — Square has draw()Key Takeaways
- Python OOP is duck-typed — implement the right methods, don’t worry about interfaces.
- Use dunder methods (
__add__,__eq__,__repr__, etc.) to integrate with Python’s operators and built-ins. super()follows the MRO (C3 linearization), not just the direct parent.- Use
_prefixfor private-by-convention. Don’t use__manglingunless you have a specific reason. - Use
@dataclassfor data-holding classes — it generates boilerplate for free. - Use
ABCfor strict contracts. UseProtocolfor structural (duck) typing with type checkers.
