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.
The Four Pillars of OOP
These four concepts are the foundation of every well-designed object-oriented system. Think of them as the load-bearing walls of a building — you can rearrange the furniture (implementation details), but these structural elements must be solid or everything collapses. In interviews, demonstrating fluency with these pillars is table stakes; what separates candidates is knowing when and why to apply each one.Encapsulation
Abstraction
Inheritance
Polymorphism
1. Encapsulation
Encapsulation protects object integrity by hiding internal state and requiring all interaction through methods. Think of it like an ATM machine: you interact through a well-defined interface (insert card, enter PIN, select amount), but the internal cash-counting mechanism, network communication, and security protocols are completely hidden. If the bank upgrades its internal systems, your interaction with the ATM does not change. That is encapsulation in action — controlled access gates that protect invariants and allow internal evolution without breaking external consumers.Benefits of Encapsulation
| Benefit | Description |
|---|---|
| Data Protection | Prevents invalid states |
| Flexibility | Change implementation without affecting users |
| Validation | Enforce business rules at access points |
| Debugging | All modifications go through known paths |
2. Abstraction
Abstraction hides complexity by showing only necessary details. Consider how you drive a car: you turn the steering wheel and the car turns. You don’t need to understand the rack-and-pinion mechanism, power steering fluid dynamics, or electronic stability control. The steering wheel is an abstraction over enormous mechanical complexity. In software, abstraction lets you define what something does (the contract) without dictating how it does it — enabling multiple implementations to coexist behind the same interface.3. Inheritance
Inheritance creates “is-a” relationships, allowing code reuse and specialization. Think of it like biological taxonomy: a Golden Retriever is a Dog, which is a Mammal, which is an Animal. Each level adds specificity while inheriting general traits. However, inheritance is the most overused and misused OOP pillar. The rule of thumb: use inheritance only when there is a genuine “is-a” relationship and the parent class is stable. If the parent changes frequently, you will end up with fragile hierarchies. When in doubt, prefer composition — it gives you flexibility without the tight coupling.When to Use Inheritance
| Use Inheritance | Use Composition |
|---|---|
| True “is-a” relationship | ”Has-a” relationship |
| Shared behavior across types | Flexible behavior mixing |
| Stable base class | Frequently changing behavior |
| Limited hierarchy depth | Complex variations |
4. Polymorphism
Polymorphism allows objects of different types to be treated uniformly. Imagine a universal remote control: pressing “play” does different things depending on whether it is controlling a DVD player, a streaming box, or a Bluetooth speaker, but you just press one button. In code, this means you can write a function that accepts aShape and calls area() without knowing whether it is a Circle, Rectangle, or Triangle. This is the foundation of extensible design — new types can be added without modifying existing logic, which directly supports the Open/Closed Principle (the “O” in SOLID).
Method Overriding (Runtime Polymorphism)
Method Overloading (Compile-time Polymorphism)
Python doesn’t have true overloading, but you can achieve similar results:Composition vs Inheritance
This is one of the most important design decisions you will face in LLD interviews. The Gang of Four said it best: “Favor composition over inheritance.” Inheritance creates a rigid hierarchy — once you commit, changing the parent class ripples through every child. Composition is like assembling capabilities from interchangeable parts, giving you runtime flexibility. Use inheritance when the relationship is stable and truly “is-a”; use composition when you need to mix and match behaviors.Interview Deep-Dive
Explain the difference between abstraction and encapsulation. Most candidates confuse them -- how would you distinguish them clearly?
Explain the difference between abstraction and encapsulation. Most candidates confuse them -- how would you distinguish them clearly?
- Encapsulation is about hiding data and controlling access. It operates at the implementation level. You make fields private and expose controlled methods (getters, setters, business methods) to protect invariants. The BankAccount class hides its balance field and only allows changes through deposit() and withdraw(), which enforce business rules.
- Abstraction is about hiding complexity and defining contracts. It operates at the design level. You define what a PaymentProcessor does (process payment, issue refund) without specifying how any particular processor implements it. Abstraction uses interfaces and abstract classes to create a wall between “what” and “how.”
- The practical difference: encapsulation prevents someone from setting account.balance = -1000 directly. Abstraction prevents someone from needing to know whether the payment goes through Stripe, PayPal, or Square.
- A good mental model: encapsulation is the lock on your front door (controls who can change your stuff). Abstraction is the thermostat (you set a desired temperature without understanding the HVAC system behind it).
You are designing a notification system. A junior developer suggests using inheritance: EmailNotification extends Notification, SMSNotification extends Notification. You suggest composition instead. Justify your choice.
You are designing a notification system. A junior developer suggests using inheritance: EmailNotification extends Notification, SMSNotification extends Notification. You suggest composition instead. Justify your choice.
- The notification channel is a behavior that varies independently of the notification itself. A single notification might need to be sent via email AND SMS AND push simultaneously. With inheritance, you would need EmailSMSNotification, EmailPushNotification, SMSPushNotification — a combinatorial explosion of subclasses.
- With composition, the Notification class holds a list of NotificationChannel objects (each implementing a send() method). At runtime, you can mix and match channels freely: add Slack, remove SMS, combine email with push. No new subclasses required.
- This is the Strategy pattern applied to a collection. The Notification delegates sending to its channel strategies, and the set of channels is configured at runtime rather than baked into the class hierarchy.
- The rule of thumb I follow: if the behavior varies independently and might be combined, use composition. If the relationship is a genuine identity (“a Dog is-a Animal” and will never need to also be a Vehicle), inheritance is fine.
In the code example, a Duck's fly_behavior is swapped at runtime from CanFly to CantFly. What SOLID principle does this directly demonstrate, and what design pattern is at work?
In the code example, a Duck's fly_behavior is swapped at runtime from CanFly to CantFly. What SOLID principle does this directly demonstrate, and what design pattern is at work?
- This directly demonstrates the Open/Closed Principle. The Duck class is closed for modification (you do not edit the Duck class to change its flying behavior) but open for extension (you inject a different FlyBehavior implementation at runtime).
- The design pattern is Strategy. The Duck holds a reference to a FlyBehavior interface, and the concrete behavior (CanFly or CantFly) is injected and can be swapped. The key is that the Duck delegates the “how to fly” decision to a separate object rather than encoding it in its own class hierarchy.
- It also supports the Dependency Inversion Principle: Duck depends on the FlyBehavior abstraction, not on a concrete CanFly or CantFly class.
- In a real-world scenario, this pattern appears everywhere: a payment processor whose payment method can be swapped, a report generator whose output format can be changed, or a game character whose weapon can be switched mid-game.
Explain runtime polymorphism versus compile-time polymorphism. Why does Python not have true method overloading?
Explain runtime polymorphism versus compile-time polymorphism. Why does Python not have true method overloading?
- Runtime polymorphism (method overriding) is when the actual method called is determined at runtime based on the object’s type. If you have a list of Shape objects and call area() on each, Python looks up the actual class (Circle, Rectangle, Triangle) at runtime and dispatches to the correct implementation. This is the foundation of extensible design.
- Compile-time polymorphism (method overloading) is when the method to call is determined at compile time based on the parameter types or count. Languages like Java and C++ support this natively: you can define add(int, int) and add(double, double) as separate methods.
- Python does not have true overloading because it is dynamically typed. When you define two methods with the same name in a class, the second definition simply overwrites the first. Python resolves method calls at runtime using the method name only, not the parameter signature.
- In Python, you achieve similar results through default arguments, *args, **kwargs, Union types, or the functools.singledispatch decorator. The @overload decorator from typing is only for static type checkers — it does not create actual overloaded methods at runtime.
Interview Questions
Your BankAccount class uses Python's name mangling (__balance) for privacy. A senior engineer on your team says 'Python has no real private fields -- this is security theater.' Are they right, and does it matter?
Your BankAccount class uses Python's name mangling (__balance) for privacy. A senior engineer on your team says 'Python has no real private fields -- this is security theater.' Are they right, and does it matter?
- They are technically correct. Python’s double-underscore name mangling only renames
__balanceto_BankAccount__balance. Anyone who knows this convention can access it directly:account._BankAccount__balance = 1000000. It is not enforced at the runtime level the way Java’sprivateis enforced by the JVM. - But calling it “security theater” misses the point. Encapsulation is not about security — it is about intent and maintainability. The double underscore is a strong signal to other developers: “This is an internal implementation detail. If you access it directly, you own the breakage when I refactor.” It is a contract, not a lock.
- In production at scale, what matters is that all modifications go through controlled paths (deposit, withdraw) where business rules and logging are enforced. If someone bypasses that via name mangling, they have violated the contract and your audit trail breaks. This is caught by code review and linting rules (
pylintflags direct access to mangled names), not by language enforcement. - The real-world analogy: a “Do Not Enter” sign on a door will not stop a burglar, but it will stop your coworkers from accidentally walking into the server room. That is what encapsulation does in Python — it prevents accidental misuse, not malicious abuse.
- If you were building a financial system in Python where data integrity is critical, what additional mechanisms beyond name mangling would you use to protect invariants? (Testing for: knowledge of descriptors,
__slots__, frozen dataclasses, runtime validation libraries like Pydantic, or even the argument for choosing a statically-typed language.) - The
get_statement()method returnsself.__transactions.copy(). Why does it return a copy instead of the list itself, and what bug would occur if it returned the reference directly?
You are designing a Shape hierarchy with area() and perimeter() methods. A new requirement arrives: some shapes need a draw() method, but not all (a 'HeadlessShape' used in backend calculations should not know about rendering). How do you handle this without violating SOLID?
You are designing a Shape hierarchy with area() and perimeter() methods. A new requirement arrives: some shapes need a draw() method, but not all (a 'HeadlessShape' used in backend calculations should not know about rendering). How do you handle this without violating SOLID?
- This is a textbook case where inheritance alone fails and you need Interface Segregation. If you add
draw()to the Shape abstract class, HeadlessShape must implement it and either stub it out (ISP violation) or throw NotImplementedError (LSP violation). Both are design smells. - The clean solution is to separate the concerns: keep Shape as a pure geometric contract (
area(),perimeter()), and create a separateDrawableinterface with adraw()method. Classes likeCirclecan implement bothShapeandDrawable, whileHeadlessCircleimplements onlyShape. - Even better, use composition: create a
ShapeRendererthat takes a Shape and a rendering strategy. The shape knows its geometry; the renderer knows how to draw. This way, the same Circle instance can be drawn on a canvas, exported to SVG, or used in a pure calculation context — without the Circle class changing at all. - This is the Strategy pattern combined with ISP, and it mirrors how real frameworks work. Matplotlib separates Figure (data) from Backend (rendering). React separates component logic from the renderer (ReactDOM vs React Native).
draw() to Shape and make HeadlessShape raise NotImplementedError” or “Use a boolean flag is_drawable to decide whether to call draw().” Both approaches couple unrelated concerns and violate LSP or OCP.Follow-ups:- If you went with composition and a ShapeRenderer, how would you handle a shape that needs different rendering on screen versus PDF export without creating a combinatorial explosion of classes?
- Python supports multiple inheritance and mixins. Would you use a
DrawableMixinhere instead of composition, and what are the trade-offs?
The ElectricCar example calls super().drive(distance) after its own logic. What happens if a future developer inserts a PluginHybridCar between Car and ElectricCar in the hierarchy? What problem does this create and how would you prevent it?
The ElectricCar example calls super().drive(distance) after its own logic. What happens if a future developer inserts a PluginHybridCar between Car and ElectricCar in the hierarchy? What problem does this create and how would you prevent it?
- This is the fragile base class problem. When
PluginHybridCaris inserted betweenCarandElectricCar, thesuper()call inElectricCar.drive()no longer goes toCar.drive()— it goes toPluginHybridCar.drive(), which may have its own fuel-consumption logic that conflicts with the electric charge calculation. The MRO (Method Resolution Order) has silently changed, and the behavior breaks without any code in ElectricCar being modified. - In Python specifically,
super()follows the C3 linearization algorithm for MRO. With deep hierarchies, the order of method calls becomes non-obvious. You can inspect it withElectricCar.__mro__, but requiring developers to check MRO before making changes is a sign the hierarchy is too fragile. - Prevention strategies: First, keep hierarchies shallow — two levels is fine, three is a warning sign, four is almost certainly wrong. Second, favor composition: instead of ElectricCar extending Car, have a Vehicle class that holds a
Drivetraininterface (ElectricDrivetrain, HybridDrivetrain, CombustionDrivetrain). Third, if you must use inheritance, make the parent methodsfinal(in Java) or document them with a clear “do not override” contract and enforce it with tests. - The Gang of Four warned about this in 1994: “Favor composition over inheritance.” The ElectricCar example is simple enough to work, but the moment you add PluginHybrid, the hierarchy needs to be flattened into a composition-based design.
super() everywhere.” Both answers treat the symptom (wrong dispatch order) rather than the disease (fragile hierarchy depth).Follow-ups:- How does Python’s MRO (C3 linearization) differ from C++‘s multiple inheritance resolution, and why does Python’s approach prevent the diamond problem?
- If you were refactoring this Vehicle hierarchy from inheritance to composition mid-project with 50 existing subclasses, what would your migration strategy look like?
In the Composition vs Inheritance section, the Duck's fly_behavior is a public attribute that can be reassigned to any object. What are the risks of this approach in a production codebase, and how would you make it safer?
In the Composition vs Inheritance section, the Duck's fly_behavior is a public attribute that can be reassigned to any object. What are the risks of this approach in a production codebase, and how would you make it safer?
- The biggest risk is type safety. Nothing prevents someone from writing
duck.fly_behavior = "hello"orduck.fly_behavior = 42. Whenduck.fly()is called later, it will crash with an AttributeError at runtime, potentially deep in a call stack where the root cause is hard to trace. The bug is introduced on one line but manifests on a completely different line, possibly in a different module. - A second risk is the open assignment window. Between creating the Duck and assigning a valid fly_behavior, the object is in an invalid state. If any code calls
duck.fly()during that window, you get a crash. This is the “temporal coupling” antipattern. - To make it safer in Python, I would use a property with a setter that validates the type:
@fly_behavior.setterthat checksisinstance(value, FlyBehavior)and raisesTypeErrorif it fails. I would also require the behavior in__init__so a Duck cannot be created without one. - For maximum safety, I would use a
Protocolfromtypinginstead of an ABC, which enables structural subtyping (duck typing, appropriately enough). Any object with afly()method matches the protocol, but static type checkers like mypy will catch mismatches before runtime. - In production at Uber or Stripe, you would see this pattern wrapped in a
set_behavior()method that logs the change, validates the input, and possibly emits a metric. Direct attribute assignment is fine for tutorials but dangerous in production where auditability matters.
- What is the difference between Python’s
ABC(abstract base class) andProtocolfor defining the FlyBehavior contract, and when would you choose one over the other? - If the Duck has 5 different swappable behaviors (fly, swim, quack, eat, sleep), how do you prevent the constructor from becoming a 5-parameter monster?
You said polymorphism lets you write a function that accepts a Shape and calls area() without knowing the concrete type. But what if a new developer adds a FractalShape where area() is computationally expensive (takes 30 seconds)? Your total_area() function now hangs. How does this change your design?
You said polymorphism lets you write a function that accepts a Shape and calls area() without knowing the concrete type. But what if a new developer adds a FractalShape where area() is computationally expensive (takes 30 seconds)? Your total_area() function now hangs. How does this change your design?
- This is a subtle but critical violation of the Liskov Substitution Principle — not at the type level, but at the behavioral contract level. The implicit contract of
area()is that it returns quickly. FractalShape honors the method signature but violates the performance expectation. Any code that iterates over aList[Shape]and callsarea()assumes roughly uniform cost. - The immediate fix is to separate the contract. Add a
CachedShapewrapper or require thatarea()always returns a precomputed value. The expensive computation happens in acompute_area()method that is called explicitly during initialization or in a background process, andarea()simply returns the cached result. - A more robust design uses the concept of “cost-aware interfaces.” You can add an
is_expensive()method to Shape, or better yet, separate Shape intoShape(fast, precomputed) andComputedShape(may be slow, needs explicit invocation). Thetotal_area()function only acceptsShape, forcing callers to precompute before passing in. - In production, this maps to real problems. At Netflix, a “simple” method like
getRecommendations()might be backed by a quick cache lookup for most users but a 2-second ML inference for cold-start users. The solution is always the same: make performance characteristics explicit in the interface, never hide latency behind a uniform abstraction.
- How would you design the Shape interface differently if you knew upfront that some implementations would be computationally expensive? Would you use async, lazy evaluation, or something else?
- This scenario reveals that LSP is about more than just types — it is about behavioral contracts. Can you give another example where a subclass honors the type signature but violates the behavioral contract?
Walk me through how you decide between using an abstract class versus an interface (Protocol in Python) when designing a class hierarchy. Give me a concrete example where you would choose each.
Walk me through how you decide between using an abstract class versus an interface (Protocol in Python) when designing a class hierarchy. Give me a concrete example where you would choose each.
- The core distinction is shared implementation. An abstract class (ABC in Python) is appropriate when you have a family of related types that share some concrete behavior. The abstract class provides the shared implementation and forces subclasses to implement the parts that vary. An interface (Protocol in Python) is appropriate when you want to define a capability contract without any shared implementation, and especially when unrelated types might share that capability.
- Concrete example for ABC: PaymentProcessor. All payment processors need to validate the amount (shared logic), but each processes payment differently (Stripe vs PayPal vs Square). The ABC has a concrete
validate_amount()method and an abstractprocess_payment()method. This avoids duplicating the validation logic across every subclass. - Concrete example for Protocol: Serializable. A User, an Order, and a LogEntry are completely unrelated types, but they all need a
to_json()method. An ABC would force them into a fake inheritance hierarchy. A Protocol says “any object with ato_json()method satisfies this contract,” enabling structural subtyping without coupling. - In Python specifically, Protocol has the additional advantage of working with third-party classes you do not control. If a library’s class happens to have a
to_json()method, it satisfies your Serializable protocol without the library needing to explicitly inherit from your ABC. ABCs require explicit registration or inheritance. - The rule I follow: if I am modeling a family of related things, use ABC. If I am modeling a capability that crosses family boundaries, use Protocol.
- Java has both abstract classes and interfaces (with default methods since Java 8). How does that change the decision compared to Python where we have ABC and Protocol?
- You mentioned third-party classes satisfying a Protocol. Can you walk me through a real scenario where this retroactive conformance saved you from writing adapter code?
A junior developer writes a class that inherits from both list and dict to create a 'ListDict' that behaves like both. What problems will they encounter, and how would you guide them toward a better design?
A junior developer writes a class that inherits from both list and dict to create a 'ListDict' that behaves like both. What problems will they encounter, and how would you guide them toward a better design?
- This is a classic abuse of multiple inheritance.
listanddicthave conflicting semantics for fundamental operations:__contains__checks values in list but checks keys in dict.__iter__yields values in list but yields keys in dict.len()counts elements in list but counts key-value pairs in dict. The resulting ListDict has ambiguous behavior and will confuse every consumer. - Python’s MRO will “resolve” the conflict by picking one parent’s method over the other based on the order of inheritance (
class ListDict(list, dict)vsclass ListDict(dict, list)), but the resolution is arbitrary from the domain perspective. The developer will spend hours debugging why iteration behaves like a list in one case and like a dict in another. - The correct approach is composition: create a class that contains a list and a dict internally, and explicitly defines which operations delegate to which backing store. This makes the semantics unambiguous:
get_by_key()uses the dict,get_by_index()uses the list. - An even better question to ask the junior is: “What problem are you actually solving?” Often, the need for a ListDict indicates a missing data structure. Python’s
OrderedDictpreserves insertion order while providing key-based lookup.collections.namedtupleor a dataclass might be what they really need. The design conversation should start with the use case, not the implementation mechanism.
super() correctly” or “Just pick one parent to inherit from and copy methods from the other.” Both sidestep the fundamental design issue.Follow-ups:- Python’s MRO uses C3 linearization. If you had
class A(B, C)and both B and C define__len__, which one wins and why? How would you determine this without running the code? - When IS multiple inheritance appropriate in Python? Can you give an example of a mixin that works well and explain why it does not have the same problems as ListDict?
You are in an LLD interview and the interviewer says: 'Design a logging framework. Should Logger use inheritance or composition for its output destinations (console, file, remote server)?' Walk me through your reasoning.
You are in an LLD interview and the interviewer says: 'Design a logging framework. Should Logger use inheritance or composition for its output destinations (console, file, remote server)?' Walk me through your reasoning.
- Composition, without question, and this is one of those cases where the answer is clear-cut. A logger’s output destinations vary independently of its core behavior. You might want to log to console AND file simultaneously, or switch from file to remote server in production. Inheritance cannot model this: you would need ConsoleLogger, FileLogger, ConsoleAndFileLogger, ConsoleAndFileAndRemoteLogger — a combinatorial explosion.
- The design: Logger holds a list of
LogHandlerobjects, each implementing ahandle(log_entry)method. ConsoleHandler, FileHandler, RemoteHandler are concrete implementations. Logger iterates through its handlers and delegates. This is the Observer pattern applied to logging, and it is exactly how Python’s built-inloggingmodule works: you add handlers to a logger, each with its own formatter and filter. - The key insight is that Logger and LogHandler have different rates of change. Logger’s core logic (formatting, filtering, level checking) is stable. Handlers change frequently: new destinations are added, existing ones are reconfigured. SRP says things that change for different reasons should be separate classes.
- Where inheritance IS appropriate: the handler hierarchy itself. FileHandler and RotatingFileHandler have a genuine is-a relationship. RotatingFileHandler is a FileHandler that adds rotation logic. The base class is stable, the specialization is genuine, and the hierarchy is shallow (two levels).
- Python’s
loggingmodule uses both inheritance (Handler -> StreamHandler -> FileHandler) and composition (Logger has Handlers). Why does it use both instead of committing to one approach? - If the remote LogHandler makes network calls, how do you prevent a slow network from blocking your application’s main thread? What pattern would you use?