Abstract Classes vs Interfaces in OOP
TL;DR
Abstract classes provide partial implementation and state for related classes, while interfaces define pure contracts without implementation. Abstract classes support single inheritance; interfaces enable multiple inheritance of type. Choose abstract classes for “is-a” relationships with shared code, interfaces for “can-do” capabilities.
Core Concept
What Are Abstract Classes?
An abstract class is a class that cannot be instantiated directly and may contain both concrete methods (with implementation) and abstract methods (without implementation). Abstract classes serve as blueprints for related subclasses that share common behavior and state.
Key characteristics:
- Can have instance variables (state)
- Can provide method implementations (partial or complete)
- Supports single inheritance only (in most languages)
- Represents “is-a” relationships
What Are Interfaces?
An interface defines a contract—a set of method signatures that implementing classes must provide. Traditionally, interfaces contain no implementation or state, only method declarations.
Key characteristics:
- No instance variables (only constants)
- No method implementations (pure contract)
- Supports multiple inheritance
- Represents “can-do” capabilities
Note: Python uses abstract base classes (ABC) for both concepts. Java 8+ allows default methods in interfaces. C++ uses pure virtual functions for interfaces.
When to Use Each
Use Abstract Classes when:
- Subclasses share common implementation code
- You need to maintain state (instance variables)
- Classes have a clear hierarchical “is-a” relationship
- You want to provide default behavior that subclasses can override
Use Interfaces when:
- You need multiple inheritance of type
- Defining capabilities across unrelated classes
- Creating a pure contract without implementation
- Designing for maximum flexibility and loose coupling
The Trade-off
Abstract classes give you code reuse but limit you to single inheritance. Interfaces give you multiple inheritance but traditionally offer no code reuse. Modern languages blur this line with default methods, but the conceptual distinction remains important for design decisions.
Visual Guide
Abstract Class vs Interface Structure
classDiagram
class AbstractAnimal {
<<abstract>>
-String name
+getName() String
+makeSound()* void
+sleep() void
}
class Flyable {
<<interface>>
+fly()* void
+getAltitude()* int
}
class Dog {
+makeSound() void
}
class Bird {
+makeSound() void
+fly() void
+getAltitude() int
}
AbstractAnimal <|-- Dog
AbstractAnimal <|-- Bird
Flyable <|.. Bird
note for AbstractAnimal "Has state and partial implementation"
note for Flyable "Pure contract, no state"
Abstract classes provide shared implementation (sleep method) and state (name field). Interfaces define capabilities (Flyable) that can be added to any class. Bird inherits from AbstractAnimal but also implements Flyable.
Multiple Inheritance with Interfaces
classDiagram
class Swimmable {
<<interface>>
+swim()* void
}
class Flyable {
<<interface>>
+fly()* void
}
class Walkable {
<<interface>>
+walk()* void
}
class Duck {
+swim() void
+fly() void
+walk() void
}
Swimmable <|.. Duck
Flyable <|.. Duck
Walkable <|.. Duck
note for Duck "Implements multiple interfaces\nfor different capabilities"
Interfaces enable multiple inheritance of type. A Duck can implement Swimmable, Flyable, and Walkable interfaces, gaining multiple capabilities without the diamond problem of multiple class inheritance.
Examples
Example 1: Abstract Class for Shared Implementation
from abc import ABC, abstractmethod
class Vehicle(ABC):
"""Abstract class with shared state and behavior"""
def __init__(self, brand, model):
self.brand = brand # Shared state
self.model = model
self._is_running = False
def start(self):
"""Concrete method - shared implementation"""
self._is_running = True
print(f"{self.brand} {self.model} started")
def stop(self):
"""Concrete method - shared implementation"""
self._is_running = False
print(f"{self.brand} {self.model} stopped")
@abstractmethod
def drive(self):
"""Abstract method - must be implemented by subclasses"""
pass
class Car(Vehicle):
def drive(self):
if self._is_running:
print(f"Driving {self.brand} {self.model} on road")
else:
print("Start the car first!")
class Boat(Vehicle):
def drive(self):
if self._is_running:
print(f"Sailing {self.brand} {self.model} on water")
else:
print("Start the boat first!")
# Usage
car = Car("Toyota", "Camry")
car.drive() # Output: Start the car first!
car.start() # Output: Toyota Camry started
car.drive() # Output: Driving Toyota Camry on road
boat = Boat("Yamaha", "242X")
boat.start() # Output: Yamaha 242X started
boat.drive() # Output: Sailing Yamaha 242X on water
# This would raise TypeError:
# vehicle = Vehicle("Generic", "Model") # Can't instantiate abstract class
Why this works: Vehicle provides shared code (start/stop) and state (brand, model, _is_running) that all vehicles need. Each subclass only implements the unique behavior (drive).
Java equivalent:
abstract class Vehicle {
protected String brand;
protected boolean isRunning;
public void start() { isRunning = true; }
public abstract void drive();
}
Try it yourself: Add a fuel_level attribute to Vehicle and a concrete refuel() method. Then add an abstract get_mileage() method that each vehicle type must implement.
Example 2: Interfaces for Multiple Capabilities
from abc import ABC, abstractmethod
class Flyable(ABC):
"""Interface for flying capability"""
@abstractmethod
def fly(self):
pass
@abstractmethod
def land(self):
pass
class Swimmable(ABC):
"""Interface for swimming capability"""
@abstractmethod
def swim(self):
pass
class Walkable(ABC):
"""Interface for walking capability"""
@abstractmethod
def walk(self):
pass
# Duck can do all three things
class Duck(Flyable, Swimmable, Walkable):
def __init__(self, name):
self.name = name
def fly(self):
print(f"{self.name} is flying")
def land(self):
print(f"{self.name} landed")
def swim(self):
print(f"{self.name} is swimming")
def walk(self):
print(f"{self.name} is walking")
# Fish can only swim
class Fish(Swimmable):
def __init__(self, name):
self.name = name
def swim(self):
print(f"{self.name} is swimming")
# Airplane can fly but not swim or walk
class Airplane(Flyable):
def __init__(self, model):
self.model = model
def fly(self):
print(f"{self.model} is flying at 30,000 feet")
def land(self):
print(f"{self.model} is landing")
# Usage
duck = Duck("Donald")
duck.walk() # Output: Donald is walking
duck.swim() # Output: Donald is swimming
duck.fly() # Output: Donald is flying
fish = Fish("Nemo")
fish.swim() # Output: Nemo is swimming
# fish.fly() # Would cause AttributeError - Fish doesn't have fly()
plane = Airplane("Boeing 747")
plane.fly() # Output: Boeing 747 is flying at 30,000 feet
plane.land() # Output: Boeing 747 is landing
# Polymorphism with interfaces
def make_it_swim(swimmer: Swimmable):
swimmer.swim()
make_it_swim(duck) # Output: Donald is swimming
make_it_swim(fish) # Output: Nemo is swimming
# make_it_swim(plane) # Type error - Airplane doesn't implement Swimmable
Why this works: Interfaces define capabilities independent of class hierarchy. Duck, Fish, and Airplane are unrelated classes, but they can share interfaces based on what they can do, not what they are.
Java equivalent:
interface Flyable {
void fly();
void land();
}
class Duck implements Flyable, Swimmable, Walkable {
public void fly() { /* implementation */ }
// ... other methods
}
Try it yourself: Create a Robot class that implements Walkable and Swimmable but not Flyable. Then create a function that accepts any Walkable object and makes it walk.
Example 3: Combining Both (Real-World Scenario)
from abc import ABC, abstractmethod
# Abstract class for shared employee behavior
class Employee(ABC):
def __init__(self, name, employee_id, salary):
self.name = name
self.employee_id = employee_id
self.salary = salary
def get_details(self):
"""Concrete method - all employees share this"""
return f"{self.name} (ID: {self.employee_id})"
@abstractmethod
def calculate_bonus(self):
"""Abstract - each employee type calculates differently"""
pass
# Interface for management capability
class Manageable(ABC):
@abstractmethod
def manage_team(self):
pass
@abstractmethod
def conduct_review(self):
pass
# Interface for technical capability
class Technical(ABC):
@abstractmethod
def write_code(self):
pass
@abstractmethod
def review_code(self):
pass
# Regular developer - inherits from Employee, implements Technical
class Developer(Employee, Technical):
def calculate_bonus(self):
return self.salary * 0.10
def write_code(self):
return f"{self.name} is writing code"
def review_code(self):
return f"{self.name} is reviewing code"
# Tech lead - inherits from Employee, implements both interfaces
class TechLead(Employee, Technical, Manageable):
def __init__(self, name, employee_id, salary, team_size):
super().__init__(name, employee_id, salary)
self.team_size = team_size
def calculate_bonus(self):
return self.salary * 0.15 + (self.team_size * 1000)
def write_code(self):
return f"{self.name} is writing code (when not managing)"
def review_code(self):
return f"{self.name} is reviewing team's code"
def manage_team(self):
return f"{self.name} is managing {self.team_size} developers"
def conduct_review(self):
return f"{self.name} is conducting performance reviews"
# Usage
dev = Developer("Alice", "E001", 80000)
print(dev.get_details()) # Output: Alice (ID: E001)
print(dev.calculate_bonus()) # Output: 8000.0
print(dev.write_code()) # Output: Alice is writing code
lead = TechLead("Bob", "E002", 120000, 5)
print(lead.get_details()) # Output: Bob (ID: E002)
print(lead.calculate_bonus()) # Output: 23000.0
print(lead.write_code()) # Output: Bob is writing code (when not managing)
print(lead.manage_team()) # Output: Bob is managing 5 developers
# Polymorphism examples
def process_payroll(employee: Employee):
bonus = employee.calculate_bonus()
print(f"{employee.name}'s bonus: ${bonus}")
process_payroll(dev) # Output: Alice's bonus: $8000.0
process_payroll(lead) # Output: Bob's bonus: $23000.0
def assign_technical_task(tech_person: Technical):
print(tech_person.write_code())
assign_technical_task(dev) # Output: Alice is writing code
assign_technical_task(lead) # Output: Bob is writing code (when not managing)
Why this works: Employee abstract class provides shared state and behavior (name, ID, get_details). Interfaces (Technical, Manageable) add capabilities. TechLead gets shared code from Employee but can implement multiple interfaces for different roles.
Try it yourself: Add a Manager class that inherits from Employee and implements Manageable but NOT Technical. Then create a function that accepts any Manageable object and calls manage_team().
Common Mistakes
1. Using Abstract Classes When Interfaces Would Be Better
Mistake: Creating an abstract class for unrelated classes that share a capability.
# BAD: Abstract class for unrelated things
class Printable(ABC):
@abstractmethod
def print(self):
pass
class Document(Printable): # Makes sense
def print(self):
print("Printing document")
class Printer(Printable): # Awkward - Printer "is-a" Printable?
def print(self):
print("Printer printing")
Why it’s wrong: Printer and Document don’t have an “is-a” relationship. They share a capability (printing) but aren’t related types.
Fix: Use an interface (ABC without state) for capabilities across unrelated classes.
# GOOD: Interface for capability
class Printable(ABC):
@abstractmethod
def print(self):
pass
# Now both can implement the interface without implying inheritance
2. Putting Implementation in Interfaces (Conceptually)
Mistake: Adding state or complex logic to what should be a pure contract.
# BAD: Interface with state
class Comparable(ABC):
def __init__(self):
self.comparison_count = 0 # State in interface!
@abstractmethod
def compare_to(self, other):
pass
Why it’s wrong: Interfaces should define “what” not “how.” Adding state mixes concerns and limits flexibility.
Fix: Keep interfaces pure. Put state in abstract classes or concrete implementations.
# GOOD: Pure interface
class Comparable(ABC):
@abstractmethod
def compare_to(self, other):
pass
3. Forgetting to Implement All Abstract Methods
Mistake: Inheriting from abstract class or interface but not implementing all required methods.
class Animal(ABC):
@abstractmethod
def make_sound(self):
pass
@abstractmethod
def move(self):
pass
class Dog(Animal):
def make_sound(self):
print("Woof")
# Forgot to implement move()!
# This raises TypeError at instantiation
dog = Dog() # TypeError: Can't instantiate abstract class Dog with abstract method move
Why it’s wrong: Python won’t let you instantiate a class with unimplemented abstract methods.
Fix: Implement all abstract methods or make the subclass abstract too.
4. Overusing Abstract Classes for Single-Method Contracts
Mistake: Creating an abstract class when you only need one method contract.
# BAD: Heavy abstract class for simple contract
class Serializable(ABC):
@abstractmethod
def to_json(self):
pass
# No shared state or behavior - just a contract
Why it’s wrong: Abstract classes imply shared implementation. For pure contracts, interfaces are clearer.
Fix: Use a simple interface (ABC) for single-method contracts.
Better yet: In Python, consider using Protocol from typing for structural subtyping:
from typing import Protocol
class Serializable(Protocol):
def to_json(self) -> str:
...
5. Creating Deep Abstract Class Hierarchies
Mistake: Building multiple levels of abstract classes.
# BAD: Deep hierarchy
class Animal(ABC):
@abstractmethod
def move(self):
pass
class Mammal(Animal): # Abstract
@abstractmethod
def feed_young(self):
pass
class Carnivore(Mammal): # Abstract
@abstractmethod
def hunt(self):
pass
class Dog(Carnivore): # Finally concrete!
# Must implement move, feed_young, AND hunt
pass
Why it’s wrong: Deep hierarchies are rigid and hard to maintain. Changes ripple through all levels.
Fix: Prefer composition and interfaces. Keep hierarchies shallow (1-2 levels max).
# GOOD: Shallow hierarchy + interfaces
class Animal(ABC):
@abstractmethod
def move(self):
pass
class Huntable(ABC): # Interface
@abstractmethod
def hunt(self):
pass
class Dog(Animal, Huntable):
def move(self):
print("Running")
def hunt(self):
print("Chasing prey")
Interview Tips
1. Know the Classic Interview Question
Interviewer asks: “What’s the difference between an abstract class and an interface?”
Strong answer structure:
- Start with the key distinction: “Abstract classes can have partial implementation and state; interfaces define pure contracts.”
- Give the inheritance difference: “Abstract classes support single inheritance; interfaces enable multiple inheritance.”
- Provide a concrete example: “Use an abstract class for Vehicle with shared start/stop logic. Use interfaces like Flyable and Swimmable for capabilities that cross class boundaries.”
- Mention language specifics: “In Python, both use ABC. Java has separate keywords. C++ uses pure virtual functions for interfaces.”
Avoid: Vague answers like “Abstract classes are more flexible” without explaining why or when.
2. Be Ready to Design on the Spot
Common prompt: “Design a payment system with different payment methods.”
Approach:
- Identify shared behavior → Abstract class
Paymentwithprocess_payment()abstract method and concretelog_transaction()method - Identify capabilities → Interface
Refundablefor payment methods that support refunds - Explain your choice: “CreditCard and PayPal both inherit from Payment for shared logging, but only CreditCard implements Refundable because PayPal transactions are final.”
Show your thinking: “I’m using an abstract class here because all payments need transaction logging. I’m using an interface for Refundable because not all payment types support it, and this keeps the design flexible.”
3. Discuss Trade-offs Explicitly
When asked to choose between abstract class and interface:
Mention these trade-offs:
- Code reuse vs. flexibility: “Abstract class lets me share the validation logic, but limits me to single inheritance. If I need multiple inheritance later, I’d need to refactor.”
- Coupling: “Interfaces create looser coupling. Clients depend only on the contract, not implementation details.”
- Evolution: “Abstract classes are easier to evolve—I can add new concrete methods without breaking subclasses. Adding methods to interfaces breaks all implementations.”
Pro tip: In interviews, acknowledging trade-offs shows senior-level thinking.
4. Know Language-Specific Nuances
Be prepared to discuss:
Python:
- “Python uses ABC for both. The distinction is conceptual—no state/implementation for interfaces.”
- “Python supports multiple inheritance directly, so the interface advantage is less pronounced.”
- “Protocol from typing module offers structural subtyping as an alternative.”
Java:
- “Java 8+ allows default methods in interfaces, blurring the line.”
- “Abstract classes can have constructors; interfaces cannot.”
- “Use
abstract classkeyword vs.interfacekeyword.”
C++:
- “Pure virtual functions (= 0) create abstract classes.”
- “No separate interface keyword—use abstract classes with only pure virtual functions.”
- “Multiple inheritance is allowed but brings the diamond problem.”
5. Connect to SOLID Principles
Interviewers love this: Link your answer to design principles.
- Interface Segregation Principle (ISP): “I’d use multiple small interfaces rather than one large abstract class. Clients shouldn’t depend on methods they don’t use.”
- Dependency Inversion Principle (DIP): “Both abstract classes and interfaces let us depend on abstractions, not concretions. Interfaces are often better for DIP because they’re more abstract.”
- Liskov Substitution Principle (LSP): “Abstract classes enforce LSP through shared implementation. Subclasses inherit correct behavior.”
Example statement: “I’d use an interface here because of ISP—different clients need different subsets of functionality, and interfaces let me segregate those cleanly.”
6. Practice This Comparison Table
Memorize this for quick recall:
| Aspect | Abstract Class | Interface |
|---|---|---|
| Implementation | Partial allowed | None (pure contract) |
| State | Can have instance variables | No state |
| Inheritance | Single | Multiple |
| Relationship | ”is-a" | "can-do” |
| Use case | Shared code + hierarchy | Capabilities across types |
| Coupling | Tighter | Looser |
In interview: “Let me walk through the key differences…” then reference this table.
7. Handle the “Why Not Just Use Interfaces?” Question
Interviewer: “If interfaces are more flexible, why use abstract classes at all?”
Strong answer: “Abstract classes prevent code duplication. If I have 10 subclasses that all need the same validation logic, putting it in an abstract class means I write it once. With interfaces, I’d duplicate that logic 10 times or use composition, which adds complexity. The trade-off is worth it when subclasses are closely related and share significant behavior.”
Follow-up example: “In a game, all Enemy types need health tracking and damage calculation. An abstract Enemy class provides that shared code. But Flyable and Swimmable are interfaces because not all enemies fly or swim, and those capabilities aren’t related to being an enemy.”
Key Takeaways
-
Abstract classes provide partial implementation and state; use them when subclasses share significant code and have an “is-a” relationship. Interfaces define pure contracts without implementation; use them for “can-do” capabilities across unrelated classes.
-
Abstract classes support single inheritance; interfaces enable multiple inheritance. This is the fundamental trade-off: code reuse vs. flexibility. Choose based on whether you need shared implementation or multiple capabilities.
-
In Python, both use ABC (Abstract Base Class). The distinction is conceptual—interfaces should have no state or concrete methods. Java and C++ have separate constructs, but the design principles remain the same.
-
Prefer composition and interfaces over deep abstract class hierarchies. Shallow hierarchies (1-2 levels) with multiple interfaces are more maintainable than tall inheritance trees. This aligns with “favor composition over inheritance.”
-
For interviews, explain your choice with trade-offs. Don’t just say “I’d use an interface”—explain why (loose coupling, multiple inheritance) and what you’re giving up (no code reuse). Connect to SOLID principles when possible.