Skip to content

Python Oop

← Back to all decks

32 cards — 🟢 3 easy | 🟡 7 medium | 🔴 5 hard

🟢 Easy (3)

1. What are class methods and static methods?

Show answer @classmethod receives the class as first arg (cls), @staticmethod receives no implicit arg.

class Date:
def __init__(self, year, month, day):
self.year, self.month, self.day = year, month, day

@classmethod
def from_string(cls, s):
y, m, d = map(int, s.split('-'))
return cls(y, m, d) # works with subclasses too

@staticmethod
def is_valid(s):
parts = s.split('-')
return len(parts) == 3

Date.from_string('2026-04-01') # creates Date instance
Date.is_valid('2026-04-01') # True

Use classmethod for alternative constructors (factory methods). Use staticmethod for utility functions that don't need class/instance state but logically belong to the class.

2. What is duck typing and how does Python use it?

Show answer 'If it walks like a duck and quacks like a duck, it's a duck.' Python checks behavior (methods/attributes) rather than type.

def get_length(obj):
return len(obj) # works with str, list, dict, any object with __len__

get_length('hello') # 5
get_length([1,2,3]) # 3

Protocol classes (Python 3.8+) formalize this:
from typing import Protocol

class Sized(Protocol):
def __len__(self) -> int: ...

def f(x: Sized) -> int:
return len(x)

Duck typing enables polymorphism without inheritance. The typing.Protocol class adds optional static checking while preserving runtime duck typing.

3. What is repr vs str?

Show answer __repr__ is for developers (unambiguous), __str__ is for users (readable).

class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f'Point({self.x}, {self.y})' # eval-able if possible
def __str__(self):
return f'({self.x}, {self.y})'

p = Point(1, 2)
repr(p) # 'Point(1, 2)'
str(p) # '(1, 2)'
print(p) # calls __str__: (1, 2)

If only one is defined, implement __repr__ — str() falls back to __repr__, but not vice versa. f-strings and print() call __str__; the REPL and containers call __repr__.

🟡 Medium (7)

1. What is currying in Python?

Show answer Currying transforms a function with multiple arguments into a sequence of functions each taking one argument.

from functools import partial

def multiply(x, y):
return x * y

double = partial(multiply, 2)
print(double(5)) # 10

# Manual currying:
def curry_multiply(x):
def inner(y):
return x * y
return inner

triple = curry_multiply(3)
print(triple(5)) # 15

functools.partial is the standard way to do partial application in Python. True currying (auto-currying) isn't built-in but libraries like toolz provide it.

2. What is slots and when should you use it?

Show answer __slots__ restricts instance attributes to a fixed set, replacing the per-instance __dict__ with a more memory-efficient structure.

class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y

p = Point(1, 2)
p.z = 3 # AttributeError!

Benefits: ~40% less memory per instance, slightly faster attribute access. Use when creating millions of instances. Drawbacks: no dynamic attributes, complications with multiple inheritance, no __dict__ for introspection. Not needed for most classes.

3. What is the ABC module and how do you use abstract classes?

Show answer The abc module provides Abstract Base Classes — classes that can't be instantiated and enforce method implementation in subclasses.

from abc import ABC, abstractmethod

class Shape(ABC):
@abstractmethod
def area(self) -> float:
...

@abstractmethod
def perimeter(self) -> float:
...

class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return 3.14159 * self.r ** 2
def perimeter(self):
return 2 * 3.14159 * self.r

Shape() # TypeError: Can't instantiate abstract class
Circle(5).area() # 78.5

Use ABCs to define interfaces and ensure subclasses implement required methods. Also supports @abstractproperty (deprecated — use @property + @abstractmethod).

4. Dataclasses vs namedtuples — when to use which?

Show answer Both reduce boilerplate for data-holding classes but differ in mutability and features.

from dataclasses import dataclass
from typing import NamedTuple

@dataclass
class PointDC:
x: float
y: float

class PointNT(NamedTuple):
x: float
y: float

NamedTuple: immutable, hashable, tuple-compatible, lighter memory. Use for simple records, dict keys, function returns.

Dataclass: mutable by default (frozen=True for immutable), supports default_factory, __post_init__, inheritance, field metadata. Use for domain objects needing methods or validation.

Rule of thumb: NamedTuple for simple data, dataclass for everything else.

5. Explain name mangling with double underscores

Show answer Python mangles attributes starting with __ (double underscore) by prepending _ClassName to prevent accidental override in subclasses.

class Parent:
def __init__(self):
self.__secret = 42

class Child(Parent):
def __init__(self):
super().__init__()
self.__secret = 99 # different attribute!

c = Child()
print(c._Parent__secret) # 42
print(c._Child__secret) # 99

This is NOT access control — it's name collision avoidance. Single underscore (_name) is the convention for 'private'. Use __ only when you need to prevent subclass attribute clashes.

6. How does super() work in Python 3?

Show answer super() returns a proxy object that delegates method calls to the next class in the MRO.

class A:
def greet(self):
return 'A'

class B(A):
def greet(self):
return 'B->' + super().greet()

class C(A):
def greet(self):
return 'C->' + super().greet()

class D(B, C):
def greet(self):
return 'D->' + super().greet()

D().greet() # 'D->B->C->A'

Python 3 super() needs no args (uses __class__ cell). It follows MRO, not parent — crucial for cooperative multiple inheritance. Always call super().__init__() in __init__ for MI to work correctly.

7. Explain Python's property decorator

Show answer @property creates managed attributes with getter/setter/deleter methods.

class Temperature:
def __init__(self, celsius):
self._celsius = celsius

@property
def fahrenheit(self):
return self._celsius * 9/5 + 32

@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9

t = Temperature(100)
print(t.fahrenheit) # 212.0
t.fahrenheit = 32
print(t._celsius) # 0.0

Properties are descriptors under the hood. Use them to add validation, computed attributes, or to migrate from public attributes to managed access without breaking the API.

🔴 Hard (5)

1. What are metaclasses and when would you use them?

Show answer A metaclass is the class of a class — it controls how classes themselves are created. The default metaclass is `type`.

class Meta(type):
def __new__(mcs, name, bases, namespace):
# modify class before creation
namespace['created_by'] = 'Meta'
return super().__new__(mcs, name, bases, namespace)

class MyClass(metaclass=Meta):
pass

print(MyClass.created_by) # 'Meta'

Use cases: ORMs (Django models), API registration, enforcing interfaces. In practice, __init_subclass__ or class decorators cover most use cases more simply.

2. Implement the Singleton pattern in Python (three ways)

Show answer 1. Module-level instance (Pythonic):
_instance = MyClass()

2. __new__ override:
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance

3. Metaclass:
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kw):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kw)
return cls._instances[cls]

The module-level approach is simplest and most Pythonic. Use __new__ for classes that need lazy initialization.

3. Explain Python's Method Resolution Order (MRO)

Show answer MRO determines the order in which base classes are searched when calling a method. Python uses the C3 linearization algorithm.

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

print(D.__mro__) # (D, B, C, A, object)

Rules: (1) children before parents, (2) left-to-right order preserved, (3) each class appears once. Use ClassName.mro() or ClassName.__mro__ to inspect. super() follows the MRO, not just the immediate parent — critical for cooperative multiple inheritance.

4. What are descriptors in Python?

Show answer A descriptor is any object that defines __get__, __set__, or __delete__. They control attribute access on classes.

class Validator:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, int):
raise TypeError(f'{self.name} must be int')
obj.__dict__[self.name] = value

class Order:
quantity = Validator()

Property, classmethod, staticmethod are all implemented as descriptors. Data descriptors (with __set__) take priority over instance __dict__; non-data descriptors don't.

5. What is init_subclass and how does it replace metaclasses?

Show answer __init_subclass__ is a hook called when a class is subclassed, added in Python 3.6. It covers many metaclass use cases more simply.

class Plugin:
registry = {}
def __init_subclass__(cls, name=None, **kwargs):
super().__init_subclass__(**kwargs)
Plugin.registry[name or cls.__name__] = cls

class PDF(Plugin, name='pdf'):
pass

class CSV(Plugin, name='csv'):
pass

print(Plugin.registry) # {'pdf': PDF, 'csv': CSV}

This auto-registers subclasses without metaclasses. Use for plugin systems, validation on subclass creation, or enforcing class-level constraints. Simpler to understand and compose than metaclasses.