Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Object-Oriented Programming (OOP)
In Python, everything is an object. Functions are objects, numbers are objects, even classes themselves are objects (they are instances oftype). OOP allows you to create your own custom types to model your problem domain.
A useful analogy: a class is like a blueprint for a house. The blueprint defines the layout (attributes) and what the house can do (methods like “open garage door”). Each house built from that blueprint is an object (instance). The blueprint is shared, but each house has its own address, paint color, and residents.
1. Classes & Objects
A Class is the blueprint. An Object is the instance.The self Parameter
In Python, you must explicitly define self as the first parameter of instance methods. It is how the method knows which object it is operating on. (It is similar to this in Java/C++, but explicit — Python’s “explicit is better than implicit” philosophy in action).
self is just a convention, not a keyword. You could call it this or me and Python would not care. But deviating from self is a surefire way to confuse every Python developer who reads your code. Follow the convention.2. Inheritance
Inheritance allows you to create specialized versions of existing classes. Think of it as an “is-a” relationship: aCat is an Animal, so it inherits everything Animal provides and can override or extend behaviors.
super()
Use super() to call methods from the parent class. This is how you extend behavior (add to it) rather than replace it.
The MRO (Method Resolution Order)
Python supports multiple inheritance — a class can inherit from more than one parent. When it does, Python uses the C3 linearization algorithm to determine which method to call. This order is called the MRO.3. Magic Methods (Dunder Methods)
Python classes can integrate tightly with language syntax using “Magic Methods” (Double UNDERscore methods, hence “dunder”). This is Python’s protocol system — by implementing specific dunder methods, your objects can behave like built-in types. Think of it as a contract: if you implement__add__, Python will call it whenever someone uses + on your object.
The most commonly used dunder methods:
| Method | Triggered By | Purpose |
|---|---|---|
__init__ | MyClass() | Initialize a new instance |
__str__ | print(obj), str(obj) | Human-readable string |
__repr__ | repr(obj), debugger display | Unambiguous string (ideally eval-able) |
__len__ | len(obj) | Return the length |
__getitem__ | obj[key] | Enable indexing and slicing |
__eq__ | obj1 == obj2 | Equality comparison |
__hash__ | hash(obj), dict key | Make object hashable |
__add__ | obj1 + obj2 | Addition operator |
__enter__/__exit__ | with obj: | Context manager protocol |
__iter__/__next__ | for x in obj: | Iterator protocol |
4. Properties
In Java, you writegetVariable() and setVariable(). In Python, we prefer direct access (obj.variable). But what if you need validation?
Use the @property decorator. It lets you use a method as if it were an attribute.
5. Dataclasses (Python 3.7+)
If you are writing a class just to hold data (like a struct in C or a record in Java), standard classes are verbose — you write__init__, __repr__, __eq__ by hand every time. Dataclasses automate this boilerplate while still giving you full control.
Frozen Dataclasses (Immutable)
__slots__ for Memory Optimization
By default, Python objects store their attributes in a __dict__ dictionary. For classes with many instances, this wastes memory. __slots__ replaces the dict with a fixed-size array, reducing memory usage by 30-40%.
6. Duck Typing and Protocols
Python follows the principle of duck typing: “If it walks like a duck and quacks like a duck, it is a duck.” You do not check what an object is — you check what it can do.Protocols are Python’s answer to Go interfaces — they define what an object must be able to do without requiring inheritance. Use them for type safety without coupling. This is the Pythonic alternative to Java-style abstract base classes in most situations.
Summary
- Classes: Encapsulate data and behavior. Use
selfexplicitly. - Inheritance: Prefer composition over inheritance. Check the MRO with
__mro__. - Magic Methods: Implement
__repr__first, then__str__. Make your objects behave like built-in types. - Properties: Add validation logic without changing the API (
@property). - Dataclasses: The modern way to define data containers. Use
frozen=Truefor immutability,field(default_factory=...)for mutable defaults, andslots=Truefor memory efficiency. - Duck Typing: Check capabilities, not types. Use
Protocolfor type-safe duck typing.
Interview Deep-Dive
What are metaclasses in Python? When would you actually use one in production, and what are the alternatives?
What are metaclasses in Python? When would you actually use one in production, and what are the alternatives?
Strong 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. By default, all classes in Python are instances of
type. When you writeclass Foo: pass, Python internally callstype("Foo", (object,), namespace)to create the class object. A custom metaclass lets you intercept and modify this class creation process. - You define a metaclass by subclassing
typeand overriding__new__or__init__.__new__is called before the class object is created (you can modify the class name, bases, or namespace).__init__is called after the class object exists (you can modify the class in place).__init_subclass__(Python 3.6+) is a lighter-weight alternative that hooks into subclass creation without a full metaclass. - Real production use cases are narrow but powerful. ORMs like Django’s Model and SQLAlchemy’s declarative base use metaclasses to inspect class attributes (field definitions) at class creation time and build database table mappings. API frameworks use them to automatically register endpoint classes. Serialization libraries use them to generate schema validation code when the class is defined rather than at runtime.
- The important nuance: metaclasses are almost always overkill. Python 3.6+ introduced
__init_subclass__, which handles 80% of the use cases that previously required metaclasses (validating subclass attributes, auto-registering subclasses, injecting behavior). Class decorators handle another 15%. Actual metaclasses are the remaining 5% — when you need to control the class namespace before the class body executes (using__prepare__), or when you need metaclass inheritance to propagate behavior automatically through a class hierarchy. - The senior answer to “should I use a metaclass?” is almost always “no, use
__init_subclass__or a class decorator first.” Metaclasses add cognitive overhead, make debugging harder (stack traces go throughtype.__new__), and create composability problems (you cannot easily combine two metaclasses). They are a power tool for framework authors, not application code.
__init_subclass__ and how does it replace metaclasses for common patterns like auto-registration?__init_subclass__is a class method that is called automatically whenever the class is subclassed. It receives the new subclass as its first argument (aftercls) plus any keyword arguments passed in the class definition.- For auto-registration:
class Plugin: _registry = {}then definedef __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs); Plugin._registry[cls.__name__] = cls. Now any class that inherits fromPluginis automatically registered. No metaclass needed, no decorator needed, and it works transparently with multiple inheritance. - The key advantage over metaclasses is composability. Multiple parent classes can each define
__init_subclass__, and they all get called via the MRO (as long as they callsuper().__init_subclass__(**kwargs)). With metaclasses, having two parent classes with different metaclasses causes aTypeErrorunless you manually create a combined metaclass.
Explain Python's Method Resolution Order (MRO) and the diamond problem. How does `super()` actually work?
Explain Python's Method Resolution Order (MRO) and the diamond problem. How does `super()` actually work?
Strong Answer:
- The MRO is the order in which Python searches for methods in a class hierarchy. For single inheritance, it is straightforward: child, parent, grandparent, …, object. For multiple inheritance, it gets complex because a class can appear in multiple inheritance chains.
- Python uses the C3 linearization algorithm to compute the MRO. C3 guarantees three properties: (1) subclasses come before their parents, (2) if a class inherits from A then B, A comes before B in the MRO, and (3) the order is consistent — the same class always appears in the same relative position. You can inspect any class’s MRO with
MyClass.__mro__orMyClass.mro(). - The diamond problem occurs when a class D inherits from B and C, and both B and C inherit from A. Without a proper MRO, A’s methods could be called twice. C3 linearization ensures A appears only once in the MRO, at the end:
D -> B -> C -> A -> object. super()does not mean “call the parent class.” It means “call the next class in the MRO.” This distinction is critical for cooperative multiple inheritance. WhenB.method()callssuper().method(), it does not callA.method()(B’s parent). It calls the next class in the MRO of the actual instance, which might beC.method()if the instance is of type D. This is how all classes in the diamond get called exactly once.- The practical implication: if you use
super(), all cooperating classes must follow the same protocol — same method signature (or use*args, **kwargsto forward unknown arguments) and all must callsuper()in turn. If any class breaks the chain by not callingsuper(), downstream classes in the MRO get skipped. This is cooperative multiple inheritance, and it requires cooperation from all participants. - In production, deep multiple inheritance hierarchies are rare and usually a design smell. Mixins (small, focused classes that add a single behavior) are the common pattern:
class APIView(AuthMixin, LoggingMixin, View). Each mixin adds one capability, andsuper()chains them correctly through the MRO.
super()? How do you debug MRO-related issues?- If neither calls
super(), only the first class in the MRO wins. The second class’s method is completely shadowed. This is not an error — Python silently resolves the conflict by MRO order. This can cause subtle bugs where a class thinks its method is being called but it never is. - To debug: first, print the MRO with
MyClass.__mro__to see the exact resolution order. Second, usesuper()explicitly with two arguments to call a specific class’s version:super(SpecificClass, self).method()calls the next class afterSpecificClassin the MRO. Third, for complex hierarchies, theinspectmodule’sgetmro()function can help, and you can trace method calls by adding logging to each class’s method to see which ones actually execute.
What is the difference between `__str__` and `__repr__`, and why should every production class implement at least `__repr__`?
What is the difference between `__str__` and `__repr__`, and why should every production class implement at least `__repr__`?
Strong Answer:
__repr__is for developers. It should return an unambiguous string representation that ideally could recreate the object:repr(Point(1, 2))should return"Point(1, 2)". It is used in the REPL, in debugger displays, in log messages, and as a fallback when__str__is not defined. The convention is thateval(repr(obj))should produce an equivalent object when possible.__str__is for end users. It returns a human-readable, “pretty” string:str(datetime.now())returns"2024-01-15 10:30:00", not the full constructor call.print()calls__str__. If__str__is not defined, Python falls back to__repr__.- Every production class should implement
__repr__because when something goes wrong at 3 AM and you are reading logs, seeing<MyObject object at 0x7f...>is useless. SeeingMyObject(id=42, status='pending', retries=3)immediately tells you what state the object was in when the error occurred. This is not a nice-to-have — it is the difference between a 10-minute debugging session and a 2-hour one. - Dataclasses and named tuples give you
__repr__for free, which is another reason to prefer them for data-holding classes. For classes with complex internal state, implement__repr__to show the most diagnostically useful fields, not every internal detail. - A subtle best practice:
__repr__output should always include the class name. IfSubClassinherits fromBaseClassand onlyBaseClassdefines__repr__, the output will say “BaseClass(…)” even forSubClassinstances. Usetype(self).__name__in your__repr__to get the actual class name dynamically.
- You must implement
__hash__and__eq__. The contract is: ifa == b, thenhash(a) == hash(b). The reverse does not need to hold (hash collisions are fine). If you break this contract, dict lookups will silently fail to find keys that exist. - For
__hash__, a common pattern is to hash a tuple of the fields that determine equality:def __hash__(self): return hash((self.x, self.y)). Use only immutable fields — if a field can change, the hash changes, and the object becomes “lost” in the hash table. - You should also implement
__repr__for debuggability, and consider__lt__if you want the objects to be sortable (needed for use withsorted(),min(),max()with multiple objects). - If you use
@dataclass(frozen=True), you get__hash__,__eq__, and__repr__for free, and thefrozenflag prevents mutation, which guarantees hash stability. This is the recommended approach for value objects that serve as dict keys.
Compare `dataclass`, `NamedTuple`, `TypedDict`, and plain `dict` for representing structured data. When do you reach for each?
Compare `dataclass`, `NamedTuple`, `TypedDict`, and plain `dict` for representing structured data. When do you reach for each?
Strong Answer:
dataclassis the go-to for most structured data. It generates__init__,__repr__,__eq__, and optionally__hash__(withfrozen=True). It supports default values, field-level metadata, post-init processing (__post_init__), and inheritance. Use it when you need a proper class with methods, validation, or behavior attached to the data.NamedTuple(fromtyping) creates an immutable tuple subclass with named fields. It is lighter weight than a dataclass — instances use less memory because they are backed by tuples, not dicts. It is hashable by default. Use it for simple, immutable records where you want tuple unpacking:x, y = point. The downside is no mutable fields and limited customization.TypedDict(fromtyping) is for typing existing dictionary patterns. It does not create a new class — it is purely a type hint that tells mypy what keys a dict should have and what types they should be. Use it when you are working with JSON data or APIs that return plain dicts and you want type safety without converting to a class.TypedDicthas zero runtime overhead.- Plain
dictis for genuinely dynamic key-value data where the keys are not known at definition time: configuration from files, JSON payloads being passed through, aggregation results. If you find yourself accessingd["name"]andd["age"]repeatedly with known keys, that is a signal to use a dataclass or NamedTuple instead — you gain autocompletion, type checking, andAttributeErrorinstead ofKeyError. - The decision framework: Is the data immutable and simple?
NamedTuple. Is the data mutable, has methods, or needs validation?dataclass. Is it a JSON blob from an external API you want to type-check but not convert?TypedDict. Is the shape truly dynamic? Plaindict. In practice, about 70% of the time the answer isdataclass.
@dataclass(slots=True) and why would you use it?- Python 3.10 added
slots=Trueto dataclasses, which automatically generates__slots__for the class. Normally, Python objects store their attributes in a per-instance__dict__(a dictionary), which uses about 100-200 bytes of overhead per instance. With__slots__, attributes are stored in a fixed-size array, eliminating the__dict__entirely. - The benefits are twofold: memory savings (roughly 30-40% per instance for small objects) and slightly faster attribute access (direct array indexing vs. hash table lookup). For a service holding 1 million user objects in memory,
slots=Truecan save 100-200MB of RAM. - The trade-off: slotted classes cannot have arbitrary attributes added dynamically (
obj.new_attr = valueraisesAttributeError). This also means libraries that rely on__dict__(some serialization libraries, some debugging tools) may not work. But for data containers, the restriction is actually a feature — it prevents accidental attribute creation from typos, likeuser.nane = "Alice"silently succeeding on a regular object but raising an error on a slotted one.