Vasu Patel logoVasu Patel
← Objects First: OOP Fundamentals
All chapters in this track
  1. 01Chapter 1: Why Object Orientation Exists
  2. 02Chapter 2: Classes, Objects, and Instances, Precisely Defined
  3. 03Chapter 3: The Four Pillars I: Encapsulation
  4. 04Chapter 4: The Four Pillars II: Abstraction
  5. 05Chapter 5: The Four Pillars III: Inheritance
  6. 06Chapter 6: The Four Pillars IV: Polymorphism
  7. 07Chapter 7: Composition vs. Inheritance
  8. 08Chapter 8: Interfaces and Abstract Classes: When to Use Which
  9. 09Chapter 9: SOLID, Part 1: Single Responsibility & Open/Closed
  10. 10Chapter 10: SOLID, Part 2: Liskov Substitution, Interface Segregation, Dependency Inversion
  11. 11Chapter 11: Basic UML for Communicating Design
  12. 12Chapter 12: Anti-Patterns: God Objects, Tight Coupling, Deep Hierarchies, the Fragile Base Class, Anemic Domain Models
  13. 13Chapter 13: OOP Idioms Across TypeScript and Python — and Where OOP Is the Wrong Tool
  14. 14Chapter 14: When OOP Breaks Down: Real-World Failure Modes
  15. 15Chapter 15: Design Exercise: Parking Lot System
  16. 16Chapter 16: Design Exercise: Library Management System
  17. 17Chapter 17: Design Exercise: Elevator Controller
  18. 18Chapter 18: OOP in Interviews: What a Strong Answer Looks Like

Chapter 8 of 18

Chapter 8: Interfaces and Abstract Classes: When to Use Which

Why this matters

Both interfaces and abstract classes let you define a contract that multiple concrete classes fulfill, and both enable polymorphism (Chapter 6). Engineers who don't have a clear rule for choosing between them end up picking whichever one they used last, which produces inconsistent codebases and awkward workarounds when the wrong one turns out not to support something they need later (like Python and TypeScript's single inheritance rule, below).

The core concept

[BEGINNER]

  • Interface — pure contract. Declares what methods (and sometimes properties) a class must have; provides no implementation at all. A class can implement any number of interfaces.
  • Abstract class — partial implementation. Can define some concrete methods (shared logic) alongside abstract methods (which subclasses must implement). A class can extend only one abstract class (in TypeScript, Python, Java, C# — all single-inheritance languages).
interface Flyable {
  fly(): void;
}

interface Swimmable {
  swim(): void;
}

// A class can implement multiple interfaces — no restriction here.
class Duck implements Flyable, Swimmable {
  fly(): void { console.log("Duck flies"); }
  swim(): void { console.log("Duck swims"); }
}
abstract class Bird {
  constructor(protected name: string) {}

  // Concrete, shared implementation — every Bird gets this for free.
  describe(): string {
    return `${this.name} is a bird`;
  }

  // Abstract — every concrete subclass must supply its own.
  abstract makeSound(): string;
}

class Sparrow extends Bird {
  makeSound(): string { return "Chirp"; }
}
from abc import ABC, abstractmethod
from typing import Protocol

# Protocol: Python's structural interface — no explicit "implements" needed
class Flyable(Protocol):
    def fly(self) -> None: ...

class Bird(ABC):  # abstract base class: partial implementation
    def __init__(self, name: str) -> None:
        self.name = name

    def describe(self) -> str:
        return f"{self.name} is a bird"

    @abstractmethod
    def make_sound(self) -> str: ...

class Sparrow(Bird):
    def make_sound(self) -> str:
        return "Chirp"

The decision

[SENIOR INSIGHT] The choice comes down to one question: do the implementing classes share actual code, or only a contract?

InterfaceAbstract class
Provides shared implementationNo — pure contractYes — concrete methods alongside abstract ones
A class can have how manyMany (multiple interfaces)One (single inheritance)
Models"Can do X" (a capability)"Is a kind of X, with shared behavior"
Best forCross-cutting capabilities unrelated types might share (Serializable, Comparable, Notifier from Chapter 7)A family of related types that share real, non-trivial logic
Cost of choosing wrongUsually low — can add more interfaces freelyHigher — you've spent your one inheritance slot; hard to change later without restructuring

[IN PRACTICE] Because a class can implement many interfaces but extend only one class, interfaces are the safer default when you're unsure — they don't foreclose other design options later. Reach for an abstract class specifically when there's real, shared, non-trivial implementation you'd otherwise have to duplicate across every implementing class — not just a shared method signature.

A common trap: single inheritance forces a choice

abstract class Vehicle {
  abstract move(): void;
}

abstract class Chargeable {
  abstract charge(): void;
}

// This does not compile — TypeScript classes can extend only one class.
// class ElectricCar extends Vehicle, Chargeable { ... }  // ERROR

// The fix: one abstract class for real shared logic, interfaces for the rest.
interface Chargeable {
  charge(): void;
}

abstract class Vehicle {
  abstract move(): void;
}

class ElectricCar extends Vehicle implements Chargeable {
  move(): void { console.log("Driving"); }
  charge(): void { console.log("Charging"); }
}

This is exactly why the "many interfaces, one abstract class" rule exists: if you'd already spent your single inheritance slot on Chargeable as an abstract class, adding a second shared-behavior need later (say, Trackable) would require restructuring the whole hierarchy. Modeling secondary capabilities as interfaces avoids painting yourself into that corner.

Common mistakes

  1. Using an abstract class purely as a contract, with no shared implementation. If every method is abstract and there's no concrete logic at all, it's just an interface with extra ceremony (and it costs the class its one inheritance slot for nothing).
  2. Using multiple interfaces where they secretly need to share real implementation, leading to the same logic copy-pasted into every implementing class. That duplication is the signal an abstract class (or composition — Chapter 7) is actually needed.
  3. Reaching for an abstract class first "by default," which risks hitting the single-inheritance wall later, as in the ElectricCar example.
  4. In Python, forgetting that Protocol gives structural typing (a class doesn't need to explicitly declare it implements the protocol — if it has the right methods, it satisfies it), while ABC requires explicit inheritance. Using ABC everywhere out of habit loses this flexibility.

Checklist: choosing between interface and abstract class

  1. Ask: is there real, non-trivial shared implementation, or only a shared method signature? Shared implementation → consider abstract class. Signature only → interface.
  2. Check whether the class might need more than one such contract. If so, at least the additional ones must be interfaces (single inheritance).
  3. Default to interfaces when unsure — they're cheaper to add later and don't consume the single-inheritance slot.
  4. In Python, prefer Protocol for capability-style contracts (structural typing, no forced inheritance) and reserve ABC for cases needing real shared logic or where enforcing explicit subclassing is actually desired.
  5. If you find the same logic duplicated across every class implementing an interface, that's a signal to introduce an abstract class (for shared implementation) or extract a composed helper (Chapter 7) — not to keep copy-pasting.

Discussion / interview questions

  1. "What's the difference between an interface and an abstract class?" Strong answer: Interface = pure contract, no implementation, multiple per class. Abstract class = partial implementation plus abstract methods, one per class (single inheritance). States both the mechanical difference and the practical consequence (interfaces don't cost your one inheritance slot).

  2. "When would you choose an abstract class over an interface?" Strong answer: When implementing classes share real, non-trivial logic that would otherwise be duplicated — not just a shared method signature.

  3. "Why might defaulting to abstract classes be risky in a single-inheritance language?" Strong answer: Using your one inheritance slot on an abstract class forecloses using it for a different shared-behavior need later; a second capability then has to be bolted on awkwardly. Interfaces avoid this because a class can implement many.

  4. "What is structural typing, and how does Python's Protocol relate to interfaces?" Strong answer: Structural typing means a type satisfies an interface by having the right shape (methods/properties), without declaring it explicitly — Protocol gives Python this; ABC instead requires explicit subclassing, closer to nominal typing.

  5. "You find the same method implementation copy-pasted across five classes that all implement the same interface. What would you consider changing?" Strong answer: Recognizes the duplication as a signal that shared implementation is needed — proposes either converting to (or introducing) an abstract class with the shared logic, or extracting the shared behavior into a composed helper object (Chapter 7), rather than continuing to copy-paste.

Chapter summary / cheat sheet

  • Interface = pure contract, no implementation, a class can implement many. Abstract class = partial implementation + abstract methods, a class can extend only one.
  • Choose based on whether implementing classes share real logic (abstract class) or only a method signature (interface).
  • Default to interfaces when unsure — they're cheaper and don't consume the single-inheritance slot.
  • Python's Protocol gives structural typing (satisfy by shape) as an alternative to ABC's explicit-inheritance model.
  • Duplicated implementation across interface implementers is a signal to introduce an abstract class or extract a composed helper — not to keep copy-pasting.