OOP

Inheritance

Inheritance lets a class derive state and behavior from another, modeling an "is-a" relationship and enabling code reuse and polymorphism.

Reuse and extend.

Inheritance lets one class (the subclass / child) reuse and extend another (the superclass / parent). The child automatically gets the parent's fields and methods and can add its own or change inherited ones.

It models an "is-a" relationship: a Dog is an Animal, so Dog can inherit eat() and sleep() from Animal and add bark().

bad.py
# Problem: duplicated behavior across "kinds of animal".
class Dog:
    def eat(self): return "nom"
    def bark(self): return "woof"

class Cat:
    def eat(self): return "nom"   # copy-paste of Dog.eat
    def meow(self): return "mew"
# Change eat() once? You must edit every class.
good.py
# Fix: put shared behavior on a parent; children inherit and extend.
class Animal:
    def eat(self):
        return "nom"              # shared by every Animal subclass

class Dog(Animal):                # Dog "is-a" Animal
    def bark(self):
        return "woof"             # Dog-only behavior

class Cat(Animal):
    def meow(self):
        return "mew"

d = Dog()
print(d.eat())                    # "nom" — inherited, not reimplemented
print(d.bark())                   # "woof" — own method

Overriding and super.

  • Overriding — a subclass redefines an inherited method to change behavior.
  • super — call the parent's version of a method or constructor.
  • Single vs multiple inheritance — one parent vs several (the latter risks the diamond problem).
  • Abstract base classes — parents that define a contract but can't be instantiated.
Overriding vs overloading Overriding replaces an inherited method (same signature) at runtime; overloading defines multiple methods with the same name but different parameters, resolved at compile time.

Fragile base class.

  • Fragile base class — changes to a parent can silently break subclasses.
  • Liskov Substitution — subclasses must be usable wherever the parent is expected, without surprising behavior.
  • Deep hierarchies — tall inheritance trees become rigid and hard to reason about.
  • Favor composition — for reuse without tight coupling, delegate to objects instead of inheriting.
Inheritance is the tightest coupling A subclass depends on its parent's implementation details. Use it for genuine is-a relationships, not just code sharing.

Interview Questions

Filter

A mechanism where one class (subclass) acquires the fields and methods of another (superclass), letting you reuse and extend existing code and model an "is-a" relationship.

The superclass (parent/base) is the class being inherited from; the subclass (child/derived) is the class that inherits, gaining the parent's members and optionally adding or overriding them.

An "is-a" relationship: the subclass is a specialized kind of the superclass (a Car is a Vehicle, a SavingsAccount is an Account). If "is-a" doesn't truly hold, inheritance is probably the wrong tool.

When a subclass provides its own implementation of a method already defined in its superclass, using the same signature. Calls on the subclass then run the overridden version instead of the inherited one.

It refers to the superclass, letting a subclass call the parent's version of a method or invoke the parent's constructor (e.g. super().__init__() / super.method()) — useful to extend rather than fully replace inherited behavior.

Code reuse: common behavior (e.g. save(), validate()) defined once in a base class is shared by all subclasses, avoiding duplication. It also enables polymorphism — treating different subclasses uniformly through the base type.

A universal base class — Object in Java/C# — from which every class implicitly inherits, providing common methods like toString()/ToString(), equals(), and hashCode().

Yes. A subclass inherits the parent's members and can also declare its own additional fields and methods, specializing or extending the parent's capabilities.

Overriding: a subclass redefines an inherited method with the same signature; the correct version is chosen at runtime based on the object's actual type (dynamic dispatch). Overloading: multiple methods share a name but have different parameter lists in the same scope; the compiler picks one at compile time based on arguments. Overriding is runtime polymorphism; overloading is compile-time (ad-hoc) polymorphism.

In multiple inheritance, if class D inherits from B and C, which both inherit from a common A, ambiguity arises over which A's members/implementation D gets (and whether A's state is duplicated). Languages resolve it differently: C++ uses virtual inheritance to share one A; Java/C# forbid multiple class inheritance (only multiple interfaces); Python uses a defined method resolution order (MRO/C3 linearization). It's a key reason many languages restrict multiple inheritance of implementation.

A class that can't be instantiated on its own and may contain abstract methods (no implementation) that subclasses must implement, plus concrete shared methods/state. Use it as a partial base for a family of closely related classes that share common behavior but must customize certain operations (Template Method pattern). It sits between a plain class and a pure interface.

Single: one subclass inherits from one superclass (A → B). Multilevel: a chain (A → B → C), where C inherits through B. Hierarchical: multiple subclasses share one superclass (B and C both extend A). Multiple: one class inherits from several parents (restricted in many languages). These describe the shape of the inheritance graph.

Because subclasses depend on a base class's implementation (not just its interface), a seemingly safe change to the base class can silently break subclasses — e.g. the base now calls an overridable method internally, or changes the order of operations, altering subclass behavior in unexpected ways. It makes base classes risky to evolve. Mitigations: minimize and document what's overridable, prefer composition, program to interfaces, and treat inheritance as a published contract.

When creating a subclass instance, constructors run from the top of the hierarchy down: the subclass constructor calls (implicitly or via super(...)) the superclass constructor first, so the parent part is fully initialized before the child's constructor body runs. This ensures inherited state is ready. If the parent lacks a no-arg constructor, the child must explicitly call an appropriate super(...).

Private members exist in the subclass instance (they're part of the object) but aren't accessible from subclass code — the subclass can't reference them directly. To expose inherited state to subclasses, the parent uses protected or provides protected/public accessors. So "inherited" and "accessible" aren't the same thing.

Mixins (Python) and traits (Scala/Rust/PHP) are units of reusable behavior that can be added to classes without a strict single-parent "is-a" hierarchy — a form of multiple inheritance of behavior. They let you compose orthogonal capabilities (e.g. Serializable, Comparable) into many classes. They offer reuse like inheritance but are more flexible/horizontal; overuse can still create complex resolution rules and hidden coupling, so they're used for cross-cutting, self-contained behaviors.

LSP states that objects of a subclass must be substitutable for their superclass without breaking correctness — code written against the base type should work with any subtype. This constrains overriding: a subclass may not strengthen preconditions or weaken postconditions, must preserve invariants, and shouldn't throw unexpected exceptions or change expected behavior. The classic violation is Rectangle/Square (setting a Square's width unexpectedly changes its height, breaking Rectangle's contract). Violating LSP makes polymorphism unsafe (callers need to know the concrete type), signaling that the inheritance relationship is wrong and often should be composition instead.

Inheritance creates the tightest coupling: a subclass depends on the base class's implementation, so it's vulnerable to the fragile base class problem, forces an "is-a" relationship that may not hold as requirements change, causes combinatorial class explosion for orthogonal variations, and is fixed at compile time. Composition (holding and delegating to other objects, "has-a") is more flexible: you can swap collaborators at runtime, combine behaviors freely, depend only on interfaces (loose coupling), and avoid inheriting unwanted members. Guideline: use inheritance only for genuine, stable is-a relationships and shared interface/contract; use composition for code reuse and combining behaviors.

MRO defines the order in which base classes are searched for a method/attribute. Python uses the C3 linearization algorithm, which produces a consistent order that respects each class's local precedence (parents in declared order) and monotonicity (a class appears before its parents), resolving the diamond so the shared ancestor appears once, after all its descendants. This makes super() calls cooperative — following the MRO chain — so each class in the hierarchy runs exactly once. Understanding MRO is essential to reason about which implementation runs and to write cooperative multiple-inheritance code correctly.

When you model multiple independent axes of variation via inheritance, you need a subclass for every combination — e.g. coffee with milk, with sugar, with both, with soy + sugar, etc. — leading to an exponential number of classes. The fix is composition-based patterns: the Decorator pattern wraps a base object to add behaviors dynamically and combinably; the Strategy pattern injects varying algorithms; the Bridge pattern separates two dimensions (abstraction and implementation) so they vary independently. These replace a rigid combinatorial hierarchy with flexible runtime composition of orthogonal features.

Interface inheritance (subtyping) means inheriting only a contract — the promise to support a set of operations — enabling polymorphism without coupling to code (implementing an interface). Implementation inheritance means inheriting actual code/state from a base class, which provides reuse but couples you to the base's implementation (fragile base class). Many design problems come from conflating them: you want the substitutability of interface inheritance but accidentally take on the coupling of implementation inheritance. Good design often separates them — depend on interfaces for polymorphism, and get reuse via composition/delegation rather than deep implementation inheritance.

For overriding to take effect, calls must be resolved by the object's actual runtime type, not its declared type. This is dynamic dispatch, typically implemented with a vtable: each object carries a pointer to its class's table of function pointers for virtual methods; a call indexes into that table to find the right override. In C++ methods are non-virtual by default (you opt in with virtual) so the base version is called on base references unless virtual; in Java methods are virtual by default. Non-virtual/static-dispatched calls are resolved at compile time and can't be overridden polymorphically. This dispatch mechanism is the engine behind runtime polymorphism.

Diagnose the pain (fragile base class, LSP violations, forced combinations, "is-a" that no longer holds). Then apply, incrementally behind tests: replace inheritance with delegation (extract the reused behavior into a collaborator the class holds and delegates to); extract interfaces so clients depend on contracts, not the hierarchy; introduce Strategy/Decorator/Bridge to turn subclass variations into composable objects; pull shared code into helper/service classes rather than a common base; and flatten the hierarchy where a subclass adds nothing meaningful. Move one responsibility at a time, keeping behavior identical, until variation is expressed through composition and small focused types instead of a tall, tightly-coupled tree.

It's when a framework's base class requires subclasses to call super.method() when overriding (e.g. always call super.onCreate()), relying on the developer to remember. This is fragile: forgetting the call silently breaks behavior, and the requirement isn't enforced by the type system. Better designs use the Template Method pattern — the base class defines a final method that does the mandatory work and calls a protected hook (an abstract or empty overridable method) for the subclass's part — so the base controls ordering and the subclass can't forget the essential step. This inverts control so correctness doesn't depend on convention.

Java uses class Dog extends Animal (and implements for interfaces). Python puts the parent in parentheses: class Dog(Animal). C++ uses class Dog : public Animal. The concept is the same; only syntax differs.

Generally no — constructors are not inherited like normal methods. A subclass defines its own constructors, which must call a superclass constructor (explicitly or implicitly) to initialize the inherited part. Some languages offer shortcuts (C++11 inheriting constructors), but by default you write the subclass's constructors.

Upcasting treats a subclass object as its superclass type (always safe, implicit) — the basis of polymorphism. Downcasting converts a superclass reference back to a subclass type (explicit and potentially unsafe — it fails/throws if the object isn't actually that subtype, so you check with instanceof/is first). Excessive downcasting often signals a design that should use polymorphism instead of type checks.

They restrict inheritance. A final class can't be subclassed and a final method can't be overridden, which prevents unwanted extension, protects invariants, and can enable optimizations. Sealed classes (Java 17+, Kotlin, C#) allow only a declared, closed set of subclasses, giving controlled extensibility and enabling exhaustive pattern matching. They express "this hierarchy is intentionally closed," improving safety and reasoning.

They describe how overridden method signatures may vary. Covariant return types: an override may return a more specific (subtype) result than the base method (allowed in Java/C++), which is safe because callers expecting the base type still get a valid value. Contravariant parameter types: in theory an override could accept a more general parameter type safely (LSP-consistent), though most mainstream languages don't allow parameter variance in overrides (they require identical parameters, or overloading). Getting variance wrong (e.g. accepting a narrower parameter) violates LSP and breaks substitutability. This ties directly to how subtyping must preserve the base contract.

Serializing an object must capture inherited state across the whole hierarchy, and deserialization must reconstruct it correctly (often requiring the base to be serializable and constructors/init to cooperate). Problems arise when a base class changes between versions (added/removed fields) — old serialized data may not map cleanly, breaking backward/forward compatibility (Java's serialVersionUID exists to manage this). Polymorphic collections need type information persisted to reconstruct the right subclass, which can be a security risk (deserialization of untrusted type data). Deep hierarchies make evolving the serialized format fragile. Mitigations: prefer explicit, versioned schemas (DTOs, protobuf) over language-native serialization of rich inheritance graphs, and keep serializable types simple and flat.