Python-Specific OOP Notes
Supplement to: OOPS_Complete_Guide.md Purpose: Translate the concepts from the main guide into
idiomatic Python, and cover Python-specific quirks interviewers may probe.
Table of Contents
1. Part 1: Fundamentals in Python
2. Part 2: Four Pillars in Python
3. Part 3: Abstract Classes & "Interfaces" in Python
4. Part 4: Python-Specific Quirks (Know These Cold)
5. Part 5: SOLID Examples in Python
6. Part 6: Design Patterns in Python
7. Part 7: Interview Answers — Python Edition
8. Part 8: Python OOP Cheat Sheet
Part 1: Fundamentals in Python
1.1 Class and Object
class Car:
# Class attribute — shared across all instances
wheels = 4
# Constructor
def __init__(self, brand, model, year):
# Instance attributes — unique per object
[Link] = brand
[Link] = model
[Link] = year
# Instance method
def start_engine(self):
print(f"{[Link]} engine started")
car1 = Car("Toyota", "Camry", 2024)
car2 = Car("Honda", "Civic", 2023)
car1.start_engine() # "Toyota engine started"
car2.start_engine() # "Honda engine started"
print([Link]) # 4 (class attribute, accessed via class)
print([Link]) # 4 (also accessible via instance)
Key Python concepts to name-drop:
• self — explicit reference to the current instance (vs implicit this in Java)
• __init__ — the constructor (not a true constructor — it initializes an already-created object;
__new__ actually creates it)
• Class attributes vs instance attributes — a common interview trap
__init__ vs __new__
class Example:
def __new__(cls, *args, **kwargs):
print("__new__ called — creates the object")
instance = super().__new__(cls)
return instance
def __init__(self, value):
print("__init__ called — initializes the object")
[Link] = value
e = Example(42)
# Output:
# __new__ called — creates the object
# __init__ called — initializes the object
You'll need __new__ for the Singleton pattern in Python.
1.2 Constructors
Python has one __init__, but you can simulate constructor overloading via:
Default arguments
class Book:
def __init__(self, title="", pages=0, author=None):
[Link] = title
[Link] = pages
[Link] = author
b1 = Book()
b2 = Book("Effective Python")
b3 = Book("Fluent Python", 792, "Luciano Ramalho")
Class methods as alternative constructors
class Date:
def __init__(self, year, month, day):
[Link] = year
[Link] = month
[Link] = day
@classmethod
def from_string(cls, date_str):
year, month, day = map(int, date_str.split('-'))
return cls(year, month, day)
@classmethod
def today(cls):
import datetime
t = [Link]()
return cls([Link], [Link], [Link])
d1 = Date(2026, 4, 22)
d2 = Date.from_string("2026-04-22")
d3 = [Link]()
This pattern (multiple @classmethod alternative constructors) is very Pythonic and worth mentioning.
1.3 Instance vs Class vs Static Methods
class Counter:
count = 0 # class attribute
def __init__(self, name):
[Link] = name # instance attribute
[Link] += 1
def show_name(self): # instance method — takes self
print([Link])
@classmethod
def get_count(cls): # class method — takes cls
return [Link]
@staticmethod
def greet(): # static method — takes nothing
print("Hello")
c1 = Counter("A")
c2 = Counter("B")
c1.show_name() # "A"
Counter.get_count() # 2
[Link]() # "Hello"
When to use each:
• Instance method — operates on instance data (needs self)
• Class method — operates on class itself (alternative constructors, modifying class state)
• Static method — utility function logically grouped with the class (no access to self or cls)
Part 2: Four Pillars in Python
2.1 Encapsulation
Python has no true private. Instead, conventions:
Syntax Meaning
name Public
_name Protected (convention — "don't touch unless you
know what you're doing")
__name Name-mangled — becomes
_ClassName__name, not truly private but
harder to access
class BankAccount:
def __init__(self):
self.__balance = 0 # name-mangled — "private"
def deposit(self, amount):
if amount > 0:
self.__balance += amount
else:
raise ValueError("Amount must be positive")
def get_balance(self):
return self.__balance
acc = BankAccount()
[Link](1000)
print(acc.get_balance()) # 1000
# print(acc.__balance) # AttributeError
print(acc._BankAccount__balance) # 1000 — name mangling isn't security
Interview phrasing: "Python uses naming conventions rather than strict access modifiers.
Double-underscore triggers name mangling, which discourages external access but doesn't prevent it —
Python follows a 'we're all consenting adults here' philosophy."
Pythonic getters/setters — use @property
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@[Link]
def celsius(self, value):
if value < -273.15:
raise ValueError("Below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32 # computed property
t = Temperature(25)
print([Link]) # 25 — looks like attribute access
[Link] = 30 # but invokes setter with validation
print([Link]) # 86.0
This is huge for interviews — @property is the Pythonic way to do getters/setters, replacing Java-style
getX()/setX() methods.
2.2 Inheritance
class Animal:
def __init__(self, name):
[Link] = name
def eat(self):
print(f"{[Link]} is eating")
class Dog(Animal): # Dog is-a Animal
def __init__(self, name, breed):
super().__init__(name) # Call parent constructor
[Link] = breed
def bark(self):
print(f"{[Link]} says Woof!")
d = Dog("Rex", "Labrador")
[Link]() # inherited
[Link]() # own
Multiple Inheritance (Python supports it)
Unlike Java, Python allows multiple class inheritance. It uses MRO (Method Resolution Order) via the C3
linearization algorithm to resolve the diamond problem.
class A:
def greet(self):
print("Hello from A")
class B(A):
def greet(self):
print("Hello from B")
class C(A):
def greet(self):
print("Hello from C")
class D(B, C): # Diamond!
pass
d = D()
[Link]() # "Hello from B"
print(D.__mro__) # (D, B, C, A, object) — MRO order
Interview point: "Python resolves the diamond problem using MRO with C3 linearization. You can inspect
it via ClassName.__mro__. The method is found by walking this list in order."
super() in Python
class A:
def __init__(self):
print("A init")
class B(A):
def __init__(self):
super().__init__() # Calls A's __init__
print("B init")
class C(B):
def __init__(self):
super().__init__() # Walks MRO: C → B → A
print("C init")
C()
# A init
# B init
# C init
2.3 Polymorphism
Runtime polymorphism (method overriding) — works the same as Java
class Animal:
def sound(self):
print("Some generic sound")
class Dog(Animal):
def sound(self):
print("Woof")
class Cat(Animal):
def sound(self):
print("Meow")
animals = [Dog(), Cat(), Animal()]
for a in animals:
[Link]() # dispatches to correct method at runtime
Duck typing — the Pythonic kind of polymorphism
"If it walks like a duck and quacks like a duck, it's a duck." Python doesn't care about types; it cares about
whether the object has the required methods.
class Duck:
def quack(self): print("Quack!")
class Person:
def quack(self): print("I'm pretending to quack!")
def make_it_quack(thing):
[Link]() # No type check — just needs a .quack() method
make_it_quack(Duck()) # "Quack!"
make_it_quack(Person()) # "I'm pretending to quack!"
Interview phrasing: "Python doesn't require formal interfaces — we use duck typing. Any object with the
right methods works, regardless of its inheritance hierarchy. This is polymorphism without the ceremony
of Java's interface declarations."
Method overloading — NOT directly supported
class Calculator:
def add(self, a, b):
return a + b
def add(self, a, b, c): # This REPLACES the previous method
return a + b + c
c = Calculator()
# [Link](1, 2) # ERROR — missing argument; only the 3-arg version exists
Python ways to simulate overloading:
1. Default arguments (simplest)
class Calculator:
def add(self, a, b, c=0):
return a + b + c
c = Calculator()
[Link](1, 2) # 3
[Link](1, 2, 3) # 6
2. *args / `kwargs`** (most flexible)
class Calculator:
def add(self, *args):
return sum(args)
c = Calculator()
[Link](1, 2) # 3
[Link](1, 2, 3, 4) # 10
3. [Link] (type-based dispatch)
from functools import singledispatchmethod
class Calculator:
@singledispatchmethod
def add(self, a, b):
raise NotImplementedError
@[Link]
def _(self, a: int, b: int):
return a + b
@[Link]
def _(self, a: str, b: str):
return a + " " + b
Interview phrasing: "Python doesn't support method overloading directly — redefining a method simply
replaces the previous one. We simulate it with default arguments, `args/*kwargs, or
[Link]` for type-based dispatch."
2.4 Abstraction
Done via the abc module (Abstract Base Classes).
from abc import ABC, abstractmethod
import math
class Shape(ABC): # Abstract class — inherits from ABC
@abstractmethod
def area(self):
pass # No implementation
def display(self): # Concrete method — shared
print(f"Area: {[Link]()}")
class Circle(Shape):
def __init__(self, radius):
[Link] = radius
def area(self): # Must implement — else TypeError
return [Link] * [Link] ** 2
class Rectangle(Shape):
def __init__(self, length, width):
[Link] = length
[Link] = width
def area(self):
return [Link] * [Link]
# s = Shape() # TypeError: Can't instantiate abstract class
c = Circle(5)
[Link]() # "Area: 78.539..."
Part 3: Abstract Classes & "Interfaces" in Python
Python has no interface keyword. All abstract contracts are done via ABC. There's no formal
distinction between abstract classes and interfaces — it's a single mechanism.
Simulating a pure interface
from abc import ABC, abstractmethod
class Drivable(ABC): # Acts like an interface
@abstractmethod
def drive(self):
pass
class Chargeable(ABC):
@abstractmethod
def charge(self):
pass
class ElectricCar(Drivable, Chargeable): # "implements" multiple
def drive(self):
print("Driving silently")
def charge(self):
print("Charging battery")
Python allows multiple inheritance, so this naturally supports the "implementing multiple interfaces"
pattern.
Protocols (PEP 544) — structural typing (Python 3.8+)
For pure duck-typed interfaces, use Protocol:
from typing import Protocol
class Drivable(Protocol):
def drive(self) -> None: ...
def operate(vehicle: Drivable):
[Link]()
class Bike: # Doesn't inherit from Drivable
def drive(self):
print("Pedaling")
operate(Bike()) # Works — structural match
When to use what:
ABC Protocol
Relationship Nominal (inheritance) Structural (duck typing)
Enforced at Runtime (TypeError on Static type checking only
instantiation)
Use when You want inheritance + shared You want flexible interfaces
code
Python's "Abstract vs Interface" answer
What to say in an interview: "Python doesn't distinguish between abstract classes and interfaces like
Java does. Both are implemented via the abc module. For pure contract-style interfaces, we can use
Protocol from the typing module, which gives us structural typing — classes don't need to inherit
from the protocol; they just need the right methods."
Part 4: Python-Specific Quirks (Know These Cold)
These are things interviewers sometimes specifically probe Python devs about.
4.1 Everything is an object
In Python, everything — including classes, functions, and types — is an object.
class Foo: pass
print(type(Foo)) # <class 'type'> — classes are instances of 'type'
print(type(type)) # <class 'type'> — type is its own metaclass
def f(): pass
print(type(f)) # <class 'function'>
This enables metaprogramming (metaclasses, decorators).
4.2 Mutable default argument trap
class BadExample:
def __init__(self, items=[]): # WRONG — list is shared!
[Link] = items
a = BadExample()
[Link](1)
b = BadExample()
print([Link]) # [1] — shares a's list!
# Fix:
class GoodExample:
def __init__(self, items=None):
[Link] = items if items is not None else []
Classic Python bug. If they ask "what's wrong with this code?" — this might be it.
4.3 is vs ==
• == — value equality (calls __eq__)
• is — identity (same object in memory)
a = [1, 2, 3]
b = [1, 2, 3]
a == b # True — same values
a is b # False — different objects
a = None
a is None # True — always compare to None with `is`
4.4 __str__ vs __repr__
• __str__ — human-readable (used by print())
• __repr__ — unambiguous, for debugging (used in REPL, repr())
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __str__(self):
return f"({self.x}, {self.y})"
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
p = Point(3, 4)
print(p) # (3, 4) — uses __str__
p # Point(x=3, y=4) — uses __repr__ in REPL
Rule of thumb: Always define __repr__; define __str__ if different is needed.
4.5 Dunder methods (operator overloading)
Python supports operator overloading via "dunder" (double-underscore) methods.
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other): # v1 + v2
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other): # v1 == v2
return self.x == other.x and self.y == other.y
def __hash__(self): # required if __eq__ defined
return hash((self.x, self.y))
def __len__(self): # len(v)
return 2
def __getitem__(self, i): # v[0], v[1]
return (self.x, self.y)[i]
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v3 = v1 + v2
print(v3.x, v3.y) # 4 6
print(len(v1)) # 2
Common dunders to know:
• __init__, __new__, __del__
• __str__, __repr__
• __eq__, __lt__, __hash__
• __add__, __sub__, __mul__
• __len__, __getitem__, __setitem__, __iter__, __next__
• __call__ (make instance callable)
• __enter__, __exit__ (context manager — with statement)
4.6 Class decorators you should know
• @staticmethod — method doesn't need class or instance
• @classmethod — method gets cls instead of self
• @property — computed attribute / getter
• @dataclass — auto-generates __init__, __repr__, __eq__ (Python 3.7+)
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
print(p) # Point(x=1, y=2) — __repr__ auto-generated
p == Point(1, 2) # True — __eq__ auto-generated
4.7 Garbage collection in Python
• Python uses reference counting primarily — when refcount hits zero, object is destroyed
• A cycle detector handles reference cycles (gc module)
• __del__ is a finalizer (roughly like destructors), but its timing is unreliable — prefer context
managers (with) for resource cleanup
Part 5: SOLID Examples in Python
Same principles, Pythonic code.
S — Single Responsibility
# ❌ Violation
class Employee:
def __init__(self, name, salary):
[Link] = name
[Link] = salary
def save_to_db(self):
pass # DB logic
def generate_report(self):
pass # Report logic
def send_email(self, msg):
pass # Email logic
# ✅ Fix
class Employee:
def __init__(self, name, salary):
[Link] = name
[Link] = salary
class EmployeeRepository:
def save(self, emp): pass
class ReportGenerator:
def generate(self, emp): pass
class EmailService:
def send(self, to, msg): pass
O — Open/Closed
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self): pass
class Circle(Shape):
def __init__(self, radius): [Link] = radius
def area(self): return [Link] * [Link] ** 2
class Rectangle(Shape):
def __init__(self, l, w): self.l, self.w = l, w
def area(self): return self.l * self.w
# Adding Triangle requires NO changes:
class Triangle(Shape):
def __init__(self, b, h): self.b, self.h = b, h
def area(self): return 0.5 * self.b * self.h
def calculate_area(shape: Shape):
return [Link]() # Polymorphism — extensible without modification
L — Liskov Substitution
# ❌ Rectangle-Square violation (same as Java example)
class Rectangle:
def __init__(self, w, h):
self._w, self._h = w, h
def set_width(self, w): self._w = w
def set_height(self, h): self._h = h
def area(self): return self._w * self._h
class Square(Rectangle):
def set_width(self, w):
self._w = w
self._h = w # forces height — breaks Rectangle's contract
def set_height(self, h):
self._w = h
self._h = h
# ✅ Fix — don't subclass when behavior doesn't match
class Shape(ABC):
@abstractmethod
def area(self): pass
class Rectangle(Shape):
def __init__(self, w, h): self._w, self._h = w, h
def area(self): return self._w * self._h
class Square(Shape):
def __init__(self, side): self._side = side
def area(self): return self._side ** 2
I — Interface Segregation
from abc import ABC, abstractmethod
# ❌ Fat interface
class Worker(ABC):
@abstractmethod
def work(self): pass
@abstractmethod
def eat(self): pass
class Robot(Worker):
def work(self): pass
def eat(self):
raise NotImplementedError("Robots don't eat") # LSP + ISP violation
# ✅ Segregated
class Workable(ABC):
@abstractmethod
def work(self): pass
class Eatable(ABC):
@abstractmethod
def eat(self): pass
class Human(Workable, Eatable):
def work(self): print("Working")
def eat(self): print("Eating")
class Robot(Workable):
def work(self): print("Working")
D — Dependency Inversion
from abc import ABC, abstractmethod
# ❌ Violation
class MySQLDatabase:
def save(self, data): print(f"Saving to MySQL: {data}")
class UserService:
def __init__(self):
[Link] = MySQLDatabase() # Hardcoded concrete dependency
def add_user(self, name):
[Link](name)
# ✅ Fix
class Database(ABC):
@abstractmethod
def save(self, data): pass
class MySQLDatabase(Database):
def save(self, data): print(f"MySQL: {data}")
class PostgresDatabase(Database):
def save(self, data): print(f"Postgres: {data}")
class UserService:
def __init__(self, db: Database): # Injected dependency
[Link] = db
def add_user(self, name):
[Link](name)
# Usage
service = UserService(MySQLDatabase())
service.add_user("Alice")
# Or swap implementation:
service = UserService(PostgresDatabase())
Python's duck typing makes DIP even more natural — you don't strictly need the Database base class;
anything with a .save() method would work. But explicit ABCs make contracts clearer.
Part 6: Design Patterns in Python
6.1 Singleton — Multiple Python Ways
Variant 1: Using __new__ (classic OOP style)
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, value=None):
# Note: __init__ runs every time Singleton() is called!
# Guard if you want init to happen once:
if not hasattr(self, '_initialized'):
[Link] = value
self._initialized = True
s1 = Singleton("first")
s2 = Singleton("second")
print(s1 is s2) # True
print([Link]) # "first" (thanks to the guard)
Variant 2: Decorator approach (Pythonic)
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Logger:
def __init__(self):
[Link] = []
def log(self, msg):
[Link](msg)
a = Logger()
b = Logger()
print(a is b) # True
Variant 3: Metaclass (advanced, purest singleton)
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
def __init__(self):
print("Creating DB connection")
d1 = Database() # "Creating DB connection"
d2 = Database() # (nothing printed — returns same instance)
print(d1 is d2) # True
Variant 4: Module-level singleton (most Pythonic)
Python modules are imported once and cached. Just put state in a module:
# [Link]
class _Logger:
def __init__(self):
[Link] = []
def log(self, msg):
[Link](msg)
logger = _Logger() # module-level instance
# [Link]
from logger import logger # always the same instance
[Link]("hello")
Thread safety
For thread-safe singletons:
import threading
class ThreadSafeSingleton:
_instance = None
_lock = [Link]()
def __new__(cls):
if cls._instance is None: # double-checked locking
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
Interview phrasing: "In Python, the most idiomatic singleton is often a module — modules are cached on
import, so anything defined at module level is effectively a singleton. For class-based singletons, I'd use a
decorator or metaclass. Double-checked locking with a [Link] handles thread safety."
6.2 Factory in Python
Simple Factory
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def draw(self): pass
class Circle(Shape):
def draw(self): print("Drawing Circle")
class Rectangle(Shape):
def draw(self): print("Drawing Rectangle")
class Square(Shape):
def draw(self): print("Drawing Square")
class ShapeFactory:
@staticmethod
def create_shape(shape_type: str) -> Shape:
shapes = {
"circle": Circle,
"rectangle": Rectangle,
"square": Square,
}
shape_cls = [Link](shape_type.lower())
if not shape_cls:
raise ValueError(f"Unknown shape: {shape_type}")
return shape_cls()
s = ShapeFactory.create_shape("circle")
[Link]() # "Drawing Circle"
Factory Method
from abc import ABC, abstractmethod
class Notification(ABC):
@abstractmethod
def notify_user(self): pass
class EmailNotification(Notification):
def notify_user(self): print("Sending email")
class SMSNotification(Notification):
def notify_user(self): print("Sending SMS")
class NotificationCreator(ABC):
@abstractmethod
def create_notification(self) -> Notification: pass
def send(self): # template method
n = self.create_notification()
n.notify_user()
class EmailCreator(NotificationCreator):
def create_notification(self):
return EmailNotification()
class SMSCreator(NotificationCreator):
def create_notification(self):
return SMSNotification()
creator = EmailCreator()
[Link]() # "Sending email"
Abstract Factory
from abc import ABC, abstractmethod
# Abstract products
class Button(ABC):
@abstractmethod
def render(self): pass
class Checkbox(ABC):
@abstractmethod
def render(self): pass
# Concrete products — Windows family
class WindowsButton(Button):
def render(self): print("Windows button")
class WindowsCheckbox(Checkbox):
def render(self): print("Windows checkbox")
# Concrete products — Mac family
class MacButton(Button):
def render(self): print("Mac button")
class MacCheckbox(Checkbox):
def render(self): print("Mac checkbox")
# Abstract factory
class GUIFactory(ABC):
@abstractmethod
def create_button(self) -> Button: pass
@abstractmethod
def create_checkbox(self) -> Checkbox: pass
class WindowsFactory(GUIFactory):
def create_button(self): return WindowsButton()
def create_checkbox(self): return WindowsCheckbox()
class MacFactory(GUIFactory):
def create_button(self): return MacButton()
def create_checkbox(self): return MacCheckbox()
class Application:
def __init__(self, factory: GUIFactory):
[Link] = factory.create_button()
[Link] = factory.create_checkbox()
def render_ui(self):
[Link]()
[Link]()
import platform
factory = WindowsFactory() if [Link]() == "Windows" else MacFactory()
app = Application(factory)
app.render_ui()
6.3 Other Patterns (Python quick examples)
Observer
class Subject:
def __init__(self):
self._observers = []
def subscribe(self, fn):
self._observers.append(fn)
def notify(self, event):
for obs in self._observers:
obs(event)
s = Subject()
[Link](lambda e: print(f"Observer 1: {e}"))
[Link](lambda e: print(f"Observer 2: {e}"))
[Link]("button clicked")
Strategy
class PaymentProcessor:
def __init__(self, strategy):
[Link] = strategy
def pay(self, amount):
[Link](amount) # functions are first-class
def credit_card(amount): print(f"Paid ${amount} via credit card")
def paypal(amount): print(f"Paid ${amount} via PayPal")
p1 = PaymentProcessor(credit_card)
[Link](100)
p2 = PaymentProcessor(paypal)
[Link](100)
Python often simplifies Strategy because functions are first-class — you don't need a full Strategy
class hierarchy.
Decorator (the pattern, not @decorator)
Python's @decorator syntax naturally implements the decorator pattern:
def logged(fn):
def wrapper(*args, **kwargs):
print(f"Calling {fn.__name__}")
result = fn(*args, **kwargs)
print(f"{fn.__name__} returned {result}")
return result
return wrapper
@logged
def add(a, b):
return a + b
add(3, 4)
# Calling add
# add returned 7
Part 7: Interview Answers — Python Edition
Cheat sheet of phrasings when asked Python-specific OOP questions:
Q: "How does Python implement encapsulation?"
"Python uses naming conventions rather than enforced access modifiers. Single underscore _name
signals 'protected by convention,' double underscore __name triggers name mangling to
_ClassName__name which discourages — but doesn't prevent — external access. The philosophy
is 'we're all consenting adults.' For controlled access, we use @property decorators."
Q: "Does Python support multiple inheritance?"
"Yes. Python uses the C3 linearization algorithm to produce a Method Resolution Order (MRO). You
can inspect it with ClassName.__mro__. The diamond problem is resolved deterministically —
super() walks the MRO."
Q: "What's the difference between abstract classes and interfaces in Python?"
"Python doesn't distinguish — both are done via the abc module with ABC and
@abstractmethod. If you want duck-typed, structural interfaces without inheritance, you can use
Protocol from the typing module, which enforces contracts at static-check time."
Q: "Does Python support method overloading?"
"Not directly — redefining a method just replaces the previous one. We simulate it with default
arguments, *args/**kwargs, or [Link] for type-based dispatch."
Q: "What's the difference between __init__ and __new__?"
"__new__ creates the object — it's the actual constructor, returns a new instance. __init__
initializes the already-created object. __new__ is rarely overridden except for singletons, immutable
types, or metaclass tricks."
Q: "How do you implement a Singleton in Python?"
"Multiple ways: a decorator, a metaclass, overriding __new__, or simply using a module — since
modules are imported once, module-level objects are effectively singletons. I'd reach for module or
decorator first; metaclass for serious use."
Q: "What are class methods vs static methods?"
"Class methods take cls and typically operate on the class itself — useful for alternative
constructors like Date.from_string(). Static methods take no class/instance reference —
they're just utility functions grouped with the class for organization."
Q: "What does @property do?"
"It lets you expose a method as an attribute. You can define getters, setters, and deleters. It's the
Pythonic replacement for Java-style getX()/setX() methods, allowing computed attributes and
validation while preserving attribute-access syntax."
Q: "Explain duck typing."
"'If it walks like a duck and quacks like a duck, it's a duck.' Python doesn't check types — if an object
has the methods your code needs, it works. This is polymorphism without requiring inheritance or
formal interfaces. It's a major reason Python APIs feel flexible."
Part 8: Python OOP Cheat Sheet
Java → Python mapping
Java Python
this self
extends class Child(Parent):
implements Inherit from ABC / Protocol
abstract class Inherit from ABC, use @abstractmethod
interface ABC with only abstract methods, or Protocol
private int x self.__x (name-mangled) or self._x
(convention)
getX()/setX() @property
static int count count = 0 (class attribute)
static void method() @staticmethod
final class No direct equivalent (can use metaclasses to
forbid subclassing)
final method No direct equivalent
[Link]() super().method()
Method overloading Default args, *args, or singledispatch
instanceof isinstance(obj, Class)
Must-know dunders
__init__ — constructor
__new__ — actual object creation
__str__ — human-readable string
__repr__ — unambiguous string (debug)
__eq__ — == operator
__hash__ — hash (needed for sets/dicts if __eq__ defined)
__lt__ — < operator
__add__ — + operator
__len__ — len()
__getitem__ — obj[i]
__iter__ — iter(obj)
__call__ — make instance callable
__enter__/__exit__ — context manager (with block)
ABC boilerplate
from abc import ABC, abstractmethod
class MyAbstract(ABC):
@abstractmethod
def do_something(self):
pass
@property pattern
class C:
def __init__(self, x):
self._x = x
@property
def x(self):
return self._x
@[Link]
def x(self, value):
if value < 0:
raise ValueError
self._x = value
Singleton quick-ref
# Decorator version
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class MyClass:
pass
Golden rules when interviewing with Python
9. Always say "self" when explaining methods — don't pretend it's implicit
10.Mention duck typing when discussing polymorphism — it's the Pythonic answer
11.Name-drop @property when discussing encapsulation
12.Use the abc module for any abstract example
13.Don't claim Python has overloading — explain how to simulate it
14.For Singleton, prefer decorator or module over __new__ in interviews
15.Know your dunders — __str__, __repr__, __eq__, __hash__, __init__, __new__ at
minimum
16.Admit Python's tradeoffs — "we don't have strict privacy, but..." — honesty > overclaiming
End of Python OOP notes. Use this alongside the main OOPS_Complete_Guide.md — the main guide
gives you the concepts, this file gives you the Python expression of those concepts.