Skip to content

Python Oop

← Back to all decks

14 cards — 🟡 9 medium | 🔴 5 hard

🟡 Medium (9)

1. How do you create abstract classes using the ABC module?

Show answer Import `ABC` and `abstractmethod` from `abc`. A class inheriting from ABC with at least one `@abstractmethod` cannot be instantiated directly.
`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`
Attempting `Shape()` raises TypeError. ABCs enforce interface contracts at instantiation time, not at class definition time.

2. What does slots do and when should you use it?

Show answer `__slots__` replaces the per-instance `__dict__` with a fixed set of attribute names, reducing memory usage and slightly speeding up attribute access.
`class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y): self.x = x; self.y = y`
With slots, you cannot add arbitrary attributes (p.z = 1 raises AttributeError). Memory savings are significant when creating millions of instances. Downsides: no `__dict__`, no dynamic attributes, inheritance complications (subclass must also define `__slots__` or gains a `__dict__`). Use for data-heavy classes; avoid for classes needing flexibility.

3. How does Python name mangling work with double-underscore attributes?

Show answer Attributes starting with `__` (double underscore) but NOT ending with `__` are "mangled" by the interpreter: `__attr` becomes `_ClassName__attr`. This prevents accidental override in subclasses.
`class Parent:
def __init__(self): self.__secret = 42
p = Parent()
# p.__secret -> AttributeError
# p._Parent__secret -> 42`
This is NOT true private access control -- it is a name collision avoidance mechanism. For conventional "private" attributes, use a single underscore `_attr`. Double underscore is mainly useful in frameworks where subclass name collisions are a real risk.

4. What is currying and how do you implement it in Python?

Show answer Currying transforms a function of multiple arguments into a sequence of functions each taking one argument. In Python, use `functools.partial` for practical currying.
`from functools import partial
def multiply(x, y): return x * y
double = partial(multiply, 2)
triple = partial(multiply, 3)
print(double(5)) # 10
print(triple(5)) # 15`
True currying (returning nested functions):
`def curry_multiply(x):
def inner(y): return x * y
return inner`
Currying is useful for creating specialized functions from general ones, callback factories, and functional-style pipelines.

5. What is the difference between encapsulation and abstraction?

Show answer Encapsulation bundles data and methods together and restricts direct access to internal state. It is about *hiding implementation details* using access modifiers (Python uses convention: `_private`, `__mangled`).
Abstraction hides complexity by exposing only the essential interface. It is about *what* an object does, not *how* it does it. ABCs and interfaces are abstraction tools.
Example: A `Database` class abstracts connection details (abstraction) while keeping the connection string as a private attribute (encapsulation). Encapsulation is a mechanism; abstraction is a design principle.

6. Deep dive: how does the @property decorator work?

Show answer `@property` turns a method into a read-only attribute. It uses the descriptor protocol under the hood.
`class Circle:
def __init__(self, r): self._r = r
@property
def radius(self): return self._r
@radius.setter
def radius(self, value):
if value < 0: raise ValueError("Negative radius")
self._r = value
@radius.deleter
def radius(self): del self._r`
`property` is actually a class implementing `__get__`, `__set__`, `__delete__`. Using it, you can add validation, computation, or lazy loading while keeping the attribute-style API. This is the Pythonic alternative to Java-style getters/setters.

7. Compare dataclasses, namedtuples, and regular classes.

Show answer **Regular class**: Full flexibility, manual `__init__`, `__repr__`, `__eq__`. Use for complex behavior.
**namedtuple**: Immutable, tuple subclass, lightweight. `Point = namedtuple('Point', ['x', 'y'])`. No default values (use `defaults=` in Python 3.6.1+). Good for simple immutable records.
**dataclass**: Mutable by default (use `frozen=True` for immutable). Auto-generates `__init__`, `__repr__`, `__eq__`. Supports defaults, type hints, `field()` for customization, `__post_init__`.
`from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float = 0.0`
Choose dataclass for most structured data; namedtuple when immutability is critical and you need tuple unpacking; regular class when you need full control.

8. When does dict exist and when does it not?

Show answer Every regular Python object has a `__dict__` storing its instance attributes. This dict is created per-instance and uses ~100-200 bytes of overhead.
Classes with `__slots__` do NOT have `__dict__` -- attributes are stored in a fixed-size struct, saving memory.
Built-in types (int, str, list) generally do not have `__dict__` either.
You can check: `hasattr(obj, '__dict__')`. A class can have BOTH `__slots__` and `__dict__` if `'__dict__'` is explicitly listed in `__slots__`, or if a parent class does not define `__slots__`.

9. What is the difference between @staticmethod and @classmethod?

Show answer `@staticmethod` defines a method that belongs to the class namespace but receives neither `self` nor `cls`. It is essentially a regular function scoped to the class.
`@classmethod` receives `cls` as the first argument and can access/modify class state. Common use: alternative constructors.
`class Date:
def __init__(self, y, m, d): self.y, self.m, self.d = y, m, d
@classmethod
def from_string(cls, s):
y, m, d = map(int, s.split('-'))
return cls(y, m, d)
@staticmethod
def is_valid(s): return len(s.split('-')) == 3`
Use `@classmethod` for factory methods; `@staticmethod` for utility functions that logically belong to the class.

🔴 Hard (5)

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

Show answer A metaclass is the "class of a class." Just as an object is an instance of a class, a class is an instance of a metaclass. The default metaclass is `type`. You define a custom metaclass by inheriting from `type` and overriding `__new__` or `__init__`.
`class MyMeta(type):
def __new__(mcs, name, bases, namespace):
# modify the class before creation
return super().__new__(mcs, name, bases, namespace)`
Use cases: ORM field registration (Django models), API validation, automatic method decoration, enforcing coding standards on all subclasses. In practice, most developers rarely need metaclasses -- prefer `__init_subclass__` or decorators first.

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

Show answer 1. **Metaclass**: `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]`
2. **Decorator**: `def singleton(cls):
instances = {}
def get(*args, **kwargs):
if cls not in instances: instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get`
3. **Module-level instance**: Python modules are singletons by design. Put your object in a module and import it -- simplest and most Pythonic approach.
Use singletons sparingly; they make testing harder due to shared state.

3. Explain MRO (Method Resolution Order) and C3 linearization.

Show answer MRO determines the order Python searches base classes when resolving a method. Python 3 uses C3 linearization, which guarantees: (1) children come before parents, (2) the order of bases in the class definition is preserved, (3) a consistent ordering exists or a TypeError is raised.
Check with `ClassName.__mro__` or `ClassName.mro()`.
Example: `class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass`
D.__mro__ is (D, B, C, A, object). The "diamond problem" is resolved by C3 -- each class appears once. Use `super()` to follow the MRO correctly rather than calling parent methods directly.

4. What are descriptors and how do get, set, delete work?

Show answer A descriptor is any object defining `__get__`, `__set__`, or `__delete__`. When a descriptor is a class attribute, Python invokes these methods on attribute access.
`class Validator:
def __set_name__(self, owner, name): self.name = name
def __get__(self, obj, objtype=None): return obj.__dict__[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()`
Data descriptors (define `__set__` or `__delete__`) take precedence over instance `__dict__`. Non-data descriptors (only `__get__`) do not. Functions are non-data descriptors -- that is how methods work.

5. What is init_subclass and how does it simplify class customization?

Show answer `__init_subclass__` is called whenever a class is subclassed, letting the parent customize subclass creation without a metaclass.
`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': , 'csv': }`
Introduced in Python 3.6. Prefer this over metaclasses for plugin registration, validation hooks, and automatic attribute injection. It is simpler, more readable, and works with standard inheritance.