Chapter 4: Object-Oriented Programming (OOP) - Interview Preparation Notes

Table of Contents

  1. Classes and Objects
  2. init, Attributes, and Methods
  3. Encapsulation and Properties
  4. Inheritance and Method Overriding
  5. Polymorphism, Duck Typing, and ABCs
  6. Magic (Dunder) Methods
  7. @classmethod and @staticmethod
  8. Dataclasses and Simple Patterns
  9. Common Interview Questions
  10. Practice Problems

1. Classes and Objects

# Basic class
class BankAccount:
    def __init__(self, owner: str, balance: float = 0.0) -> None:
        self.owner = owner
        self.balance = balance
 
    def deposit(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("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", 100)
acc.deposit(50)

Notes:

  • Objects are instances of classes.
  • Methods are functions defined inside a class that operate on instances.

2. init, Attributes, and Methods

class User:
    # Class attribute (shared among all instances)
    platform = "web"
 
    def __init__(self, name: str, email: str) -> None:
        # Instance attributes (unique per instance)
        self.name = name
        self.email = email
 
    def describe(self) -> str:
        return f"{self.name} <{self.email}> on {self.platform}"
 
# Accessing attributes
u = User("Bob", "bob@example.com")
print(u.describe())
 
# Updating class attribute affects all unless shadowed on instance
User.platform = "mobile"

Key points:

  • __init__ initializes new objects.
  • Class attributes vs instance attributes: know the difference.

3. Encapsulation and Properties

class Temperature:
    def __init__(self, celsius: float) -> None:
        self._celsius = celsius  # convention: protected (single underscore)
 
    @property
    def celsius(self) -> float:
        return self._celsius
 
    @celsius.setter
    def celsius(self, value: float) -> None:
        if value < -273.15:
            raise ValueError("Below absolute zero")
        self._celsius = value
 
    @property
    def fahrenheit(self) -> float:
        return self._celsius * 9 / 5 + 32
 
    @fahrenheit.setter
    def fahrenheit(self, value: float) -> None:
        self.celsius = (value - 32) * 5 / 9

Notes:

  • Use properties to control access/validation.
  • Name mangling: __private becomes _ClassName__private.

4. Inheritance and Method Overriding

class Shape:
    def area(self) -> float:
        raise NotImplementedError
 
class Rectangle(Shape):
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height
 
    def area(self) -> float:
        return self.width * self.height
 
class Circle(Shape):
    def __init__(self, radius: float) -> None:
        self.radius = radius
 
    def area(self) -> float:
        from math import pi
        return pi * self.radius * self.radius
 
# super() when extending behavior
class Square(Rectangle):
    def __init__(self, side: float) -> None:
        super().__init__(side, side)

Notes:

  • Prefer composition over inheritance if you only reuse small pieces.
  • Use super() to call parent methods.

5. Polymorphism, Duck Typing, and ABCs

# Duck typing: if it walks like a duck...
class FileLogger:
    def log(self, msg: str) -> None:
        print(f"FILE: {msg}")
 
class ConsoleLogger:
    def log(self, msg: str) -> None:
        print(f"CONSOLE: {msg}")
 
# Works with any object having .log()
def process(logger, message: str) -> None:
    logger.log(message)
 
process(ConsoleLogger(), "hello")
process(FileLogger(), "world")
 
# ABCs enforce required methods
from abc import ABC, abstractmethod
 
class Notifier(ABC):
    @abstractmethod
    def send(self, to: str, message: str) -> None:
        pass
 
class EmailNotifier(Notifier):
    def send(self, to: str, message: str) -> None:
        print(f"email to {to}: {message}")

Notes:

  • Duck typing is idiomatic Python; use ABCs when contracts matter.

6. Magic (Dunder) Methods

class Vector:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y
 
    def __repr__(self) -> str:
        return f"Vector(x={self.x}, y={self.y})"  # unambiguous
 
    def __str__(self) -> str:
        return f"({self.x}, {self.y})"  # user-friendly
 
    def __add__(self, other: "Vector") -> "Vector":
        return Vector(self.x + other.x, self.y + other.y)
 
    def __len__(self) -> int:
        # Example: length as count of components
        return 2
 
    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented
        return self.x == other.x and self.y == other.y

Notes:

  • Implement __repr__ for debugging and logging.
  • Return NotImplemented for unsupported comparisons.

7. @classmethod and @staticmethod

from datetime import datetime
 
class User:
    def __init__(self, name: str, joined_at: datetime) -> None:
        self.name = name
        self.joined_at = joined_at
 
    @classmethod
    def from_name(cls, name: str) -> "User":
        # alternate constructor
        return cls(name=name, joined_at=datetime.utcnow())
 
    @staticmethod
    def is_valid_name(name: str) -> bool:
        return bool(name and name.strip())
 
u = User.from_name("Alice")
assert User.is_valid_name("Bob")

Notes:

  • Use @classmethod for alternate constructors.
  • Use @staticmethod for utilities without self/cls.

8. Dataclasses and Simple Patterns

from dataclasses import dataclass
 
@dataclass(slots=True)
class Point:
    x: float
    y: float
 
p = Point(1.0, 2.0)
 
# Factory pattern (simple)
class ShapeFactory:
    @staticmethod
    def create(shape: str, **kwargs):
        shape = shape.lower()
        if shape == "circle":
            return Circle(kwargs["radius"])  # uses Circle from above
        if shape == "rectangle":
            return Rectangle(kwargs["width"], kwargs["height"])  # from above
        raise ValueError("Unknown shape")
 
# Pythonic singleton (module-level instance or caching in __new__)
class Config:
    _instance = None
 
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

Notes:

  • dataclass auto-generates __init__, __repr__, comparisons. slots=True saves memory.
  • Prefer module-level singletons instead of strict patterns when possible.

9. Common Interview Questions

  • Difference between class attributes and instance attributes?
  • When to use inheritance vs composition?
  • Explain encapsulation in Python without access modifiers.
  • What are magic methods? Commonly implemented ones?
  • Use cases for @classmethod vs @staticmethod.
  • What is duck typing? Pros/cons.

10. Practice Problems

  1. BankAccount enhancements
  • Add transfer between accounts with validation.
  • Add __str__ to print summaries.
  1. Shapes hierarchy
  • Implement Triangle with base/height and area.
  • Add perimeter where applicable.
  1. Vector arithmetic
  • Implement __sub__, __mul__ (scalar), and magnitude method.
  1. Property validation
  • Extend Temperature to support Kelvin with conversions.
  1. Notifier ABC
  • Add SMSNotifier and SlackNotifier implementations and a router that selects by type.