0% found this document useful (0 votes)
2 views25 pages

OOPS Python Notes

This document serves as a comprehensive guide to Object-Oriented Programming (OOP) in Python, covering fundamental concepts, the four pillars of OOP, abstract classes, and Python-specific quirks. It includes practical examples and explanations of key topics such as encapsulation, inheritance, polymorphism, and method overloading. Additionally, it addresses common interview questions and provides insights into Python's unique features and best practices in OOP.

Uploaded by

cheetah2nd
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views25 pages

OOPS Python Notes

This document serves as a comprehensive guide to Object-Oriented Programming (OOP) in Python, covering fundamental concepts, the four pillars of OOP, abstract classes, and Python-specific quirks. It includes practical examples and explanations of key topics such as encapsulation, inheritance, polymorphism, and method overloading. Additionally, it addresses common interview questions and provides insights into Python's unique features and best practices in OOP.

Uploaded by

cheetah2nd
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

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.

You might also like