Interfaces in OOP: When & How to Use Them

Updated 2026-03-11

TL;DR

Interfaces define what methods a class must implement without specifying how. They establish contracts that guarantee certain behaviors exist, enabling polymorphism and loose coupling. Think of them as blueprints that say “any class implementing me must have these methods.”

Prerequisites: Basic understanding of classes and objects, familiarity with methods and inheritance, knowledge of what polymorphism means at a high level.

After this topic: Implement interfaces to define contracts for classes, design systems using interface-based polymorphism, and explain when to use interfaces versus abstract classes in technical interviews.

Core Concept

What Is an Interface?

An interface is a contract that defines a set of method signatures without providing their implementation. Any class that implements an interface must provide concrete implementations for all methods declared in that interface. Interfaces answer the question: “What can this object do?” rather than “What is this object?”

Why Interfaces Matter

Interfaces enable polymorphism — the ability to treat different objects uniformly based on shared behavior. Instead of depending on concrete classes, your code depends on interfaces, making it more flexible and testable. This is the foundation of the Dependency Inversion Principle: depend on abstractions, not concretions.

Consider a payment processing system. You don’t want your code to depend on specific payment methods (CreditCard, PayPal, Bitcoin). Instead, you define a PaymentProcessor interface. Now you can add new payment methods without changing existing code.

Interfaces vs. Abstract Classes

Both define contracts, but with key differences:

  • Interfaces: Pure contracts. No implementation (in traditional OOP). A class can implement multiple interfaces.
  • Abstract Classes: Can have both abstract methods (no implementation) and concrete methods (with implementation). A class can inherit from only one abstract class.

Use interfaces when you want to define “can-do” relationships (a car can-drive, can-refuel). Use abstract classes when you want to share common implementation among related classes.

Language-Specific Notes

Python doesn’t have formal interfaces but uses Abstract Base Classes (ABC) to achieve the same goal. Java and C# have explicit interface keywords. Go uses implicit interfaces — you don’t declare implementation; if a type has the right methods, it satisfies the interface automatically.

Visual Guide

Interface Implementation Relationship

classDiagram
    class PaymentProcessor {
        <<interface>>
        +process_payment(amount)
        +refund_payment(transaction_id)
    }
    class CreditCardProcessor {
        +process_payment(amount)
        +refund_payment(transaction_id)
    }
    class PayPalProcessor {
        +process_payment(amount)
        +refund_payment(transaction_id)
    }
    class BitcoinProcessor {
        +process_payment(amount)
        +refund_payment(transaction_id)
    }
    PaymentProcessor <|.. CreditCardProcessor
    PaymentProcessor <|.. PayPalProcessor
    PaymentProcessor <|.. BitcoinProcessor

Multiple classes implementing the same interface. The dotted line indicates implementation relationship. All processors must provide process_payment and refund_payment methods.

Interface-Based Polymorphism

sequenceDiagram
    participant Client
    participant Interface as PaymentProcessor
    participant Impl1 as CreditCardProcessor
    participant Impl2 as PayPalProcessor
    Client->>Interface: process_payment(100)
    Interface->>Impl1: process_payment(100)
    Impl1-->>Interface: Success
    Interface-->>Client: Success
    Client->>Interface: process_payment(200)
    Interface->>Impl2: process_payment(200)
    Impl2-->>Interface: Success
    Interface-->>Client: Success

Client code depends on the interface, not concrete implementations. At runtime, different implementations can be swapped without changing client code.

Examples

Example 1: Payment Processing System

from abc import ABC, abstractmethod

# Define the interface (contract)
class PaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount: float) -> bool:
        """Process a payment. Returns True if successful."""
        pass
    
    @abstractmethod
    def refund_payment(self, transaction_id: str) -> bool:
        """Refund a payment. Returns True if successful."""
        pass

# Concrete implementation 1
class CreditCardProcessor(PaymentProcessor):
    def process_payment(self, amount: float) -> bool:
        print(f"Processing ${amount} via Credit Card")
        # Actual credit card processing logic here
        return True
    
    def refund_payment(self, transaction_id: str) -> bool:
        print(f"Refunding transaction {transaction_id} to Credit Card")
        return True

# Concrete implementation 2
class PayPalProcessor(PaymentProcessor):
    def process_payment(self, amount: float) -> bool:
        print(f"Processing ${amount} via PayPal")
        # Actual PayPal API call here
        return True
    
    def refund_payment(self, transaction_id: str) -> bool:
        print(f"Refunding transaction {transaction_id} via PayPal")
        return True

# Client code depends on the interface, not implementations
def checkout(processor: PaymentProcessor, amount: float):
    if processor.process_payment(amount):
        print("Payment successful!")
    else:
        print("Payment failed!")

# Usage
cc_processor = CreditCardProcessor()
paypal_processor = PayPalProcessor()

checkout(cc_processor, 99.99)
# Output:
# Processing $99.99 via Credit Card
# Payment successful!

checkout(paypal_processor, 149.99)
# Output:
# Processing $149.99 via PayPal
# Payment successful!

Key Points:

  • The checkout function doesn’t know or care which processor it’s using
  • We can add new payment methods without changing checkout
  • Python uses ABC (Abstract Base Class) to create interfaces
  • The @abstractmethod decorator enforces that subclasses must implement these methods

Java Equivalent:

interface PaymentProcessor {
    boolean processPayment(double amount);
    boolean refundPayment(String transactionId);
}

class CreditCardProcessor implements PaymentProcessor {
    public boolean processPayment(double amount) {
        System.out.println("Processing $" + amount + " via Credit Card");
        return true;
    }
    
    public boolean refundPayment(String transactionId) {
        System.out.println("Refunding " + transactionId + " to Credit Card");
        return true;
    }
}

Try it yourself: Add a BitcoinProcessor that implements the same interface. What methods must you implement?


Example 2: Data Storage Abstraction

from abc import ABC, abstractmethod
from typing import Optional, Dict

# Interface for data storage
class DataStore(ABC):
    @abstractmethod
    def save(self, key: str, value: str) -> None:
        """Save a key-value pair."""
        pass
    
    @abstractmethod
    def load(self, key: str) -> Optional[str]:
        """Load a value by key. Returns None if not found."""
        pass
    
    @abstractmethod
    def delete(self, key: str) -> bool:
        """Delete a key. Returns True if key existed."""
        pass

# Implementation 1: In-memory storage
class MemoryStore(DataStore):
    def __init__(self):
        self._data: Dict[str, str] = {}
    
    def save(self, key: str, value: str) -> None:
        self._data[key] = value
        print(f"Saved to memory: {key} = {value}")
    
    def load(self, key: str) -> Optional[str]:
        return self._data.get(key)
    
    def delete(self, key: str) -> bool:
        if key in self._data:
            del self._data[key]
            return True
        return False

# Implementation 2: File-based storage
class FileStore(DataStore):
    def __init__(self, filename: str):
        self.filename = filename
    
    def save(self, key: str, value: str) -> None:
        # Simplified: in real code, use proper file handling
        print(f"Saved to file {self.filename}: {key} = {value}")
        with open(self.filename, 'a') as f:
            f.write(f"{key}:{value}\n")
    
    def load(self, key: str) -> Optional[str]:
        try:
            with open(self.filename, 'r') as f:
                for line in f:
                    k, v = line.strip().split(':', 1)
                    if k == key:
                        return v
        except FileNotFoundError:
            pass
        return None
    
    def delete(self, key: str) -> bool:
        # Simplified implementation
        print(f"Deleted from file {self.filename}: {key}")
        return True

# Application code that uses the interface
class UserPreferences:
    def __init__(self, store: DataStore):
        self.store = store  # Depends on interface, not concrete class
    
    def set_theme(self, theme: str):
        self.store.save("theme", theme)
    
    def get_theme(self) -> str:
        return self.store.load("theme") or "default"

# Usage with different storage backends
memory_prefs = UserPreferences(MemoryStore())
memory_prefs.set_theme("dark")
print(f"Theme: {memory_prefs.get_theme()}")
# Output:
# Saved to memory: theme = dark
# Theme: dark

file_prefs = UserPreferences(FileStore("prefs.txt"))
file_prefs.set_theme("light")
print(f"Theme: {file_prefs.get_theme()}")
# Output:
# Saved to file prefs.txt: theme = light
# Theme: light

Key Points:

  • UserPreferences doesn’t know if data is stored in memory, files, or a database
  • Easy to test: pass a MemoryStore in tests instead of hitting real files
  • Easy to extend: add DatabaseStore without changing UserPreferences
  • This is dependency injection — we inject the dependency through the constructor

C++ Equivalent:

class DataStore {
public:
    virtual void save(const std::string& key, const std::string& value) = 0;
    virtual std::optional<std::string> load(const std::string& key) = 0;
    virtual bool delete(const std::string& key) = 0;
    virtual ~DataStore() = default;  // Virtual destructor important!
};

class MemoryStore : public DataStore {
    std::map<std::string, std::string> data;
public:
    void save(const std::string& key, const std::string& value) override {
        data[key] = value;
    }
    // ... other methods
};

Try it yourself: Create a DatabaseStore class that implements the DataStore interface. What would the save method do differently?

Common Mistakes

1. Forgetting to Implement All Interface Methods

class IncompleteProcessor(PaymentProcessor):
    def process_payment(self, amount: float) -> bool:
        return True
    # Missing refund_payment method!

# This will raise TypeError when you try to instantiate:
# processor = IncompleteProcessor()  # TypeError: Can't instantiate abstract class

Why it’s wrong: Interfaces are contracts. If you don’t implement all methods, you’ve broken the contract. Python’s ABC module catches this at instantiation time.

Fix: Implement all abstract methods, even if some just raise NotImplementedError for now.


2. Putting Implementation Logic in the Interface

# BAD: Interface with implementation
class PaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount: float) -> bool:
        # Don't do this!
        if amount < 0:
            raise ValueError("Amount must be positive")
        return True

Why it’s wrong: Interfaces should define behavior, not implement it. If you need shared logic, use an abstract class instead, or use composition.

Fix: Keep interfaces pure. Move shared validation to a separate helper function or base class.


3. Making Interfaces Too Large (Interface Segregation Violation)

# BAD: One giant interface
class VehicleOperations(ABC):
    @abstractmethod
    def start_engine(self): pass
    
    @abstractmethod
    def fly(self): pass  # Not all vehicles fly!
    
    @abstractmethod
    def sail(self): pass  # Not all vehicles sail!
    
    @abstractmethod
    def drive(self): pass

Why it’s wrong: A Car shouldn’t have to implement fly() and sail(). This violates the Interface Segregation Principle — clients shouldn’t depend on methods they don’t use.

Fix: Split into smaller, focused interfaces:

class Drivable(ABC):
    @abstractmethod
    def drive(self): pass

class Flyable(ABC):
    @abstractmethod
    def fly(self): pass

class Car(Drivable):  # Only implements what it needs
    def drive(self):
        print("Driving on road")

class Airplane(Drivable, Flyable):  # Can implement multiple interfaces
    def drive(self):
        print("Taxiing on runway")
    
    def fly(self):
        print("Flying in air")

4. Confusing Interfaces with Type Hints

# This is NOT an interface:
def checkout(processor: PaymentProcessor, amount: float):
    processor.process_payment(amount)

# This will NOT raise an error:
checkout("not a processor", 100)  # Python won't stop you!

Why it’s wrong: Type hints in Python are just hints — they don’t enforce anything at runtime. You need ABC to actually enforce the contract.

Fix: Use ABC for interfaces, and optionally use type checkers like mypy to catch issues before runtime.


5. Not Using Interfaces for Testability

# BAD: Directly depending on concrete class
class OrderService:
    def __init__(self):
        self.payment = CreditCardProcessor()  # Hard-coded dependency!
    
    def place_order(self, amount):
        self.payment.process_payment(amount)

# Now you can't test without hitting real credit card API!

Why it’s wrong: Hard-coded dependencies make testing difficult and code inflexible.

Fix: Depend on the interface and inject the dependency:

class OrderService:
    def __init__(self, payment_processor: PaymentProcessor):
        self.payment = payment_processor  # Injected dependency
    
    def place_order(self, amount):
        self.payment.process_payment(amount)

# In tests, use a mock:
class MockProcessor(PaymentProcessor):
    def process_payment(self, amount):
        return True  # Always succeeds in tests
    def refund_payment(self, transaction_id):
        return True

test_service = OrderService(MockProcessor())

Interview Tips

What Interviewers Look For

1. Can you explain WHY interfaces matter?

Don’t just say “they define contracts.” Explain the benefits:

  • Polymorphism: Treat different objects uniformly
  • Loose coupling: Code depends on abstractions, not concrete classes
  • Testability: Easy to mock dependencies
  • Extensibility: Add new implementations without changing existing code

Example answer: “Interfaces let me write code that depends on behavior, not specific implementations. This makes my code more flexible and testable. For instance, if I have a PaymentProcessor interface, I can easily swap between credit card, PayPal, or Bitcoin processors without changing my checkout logic.”


2. Interface vs. Abstract Class Question

This is a classic interview question. Memorize this distinction:

AspectInterfaceAbstract Class
MethodsAll abstract (traditionally)Can mix abstract and concrete
Multiple inheritanceYes (implement many)No (inherit from one)
Use case”Can-do” relationships”Is-a” relationships with shared code
StateNo instance variablesCan have instance variables

Interview answer template: “I’d use an interface when I want to define a contract without any implementation, especially when unrelated classes need the same behavior. I’d use an abstract class when I have related classes that share common implementation code.”


**3. Design Question: “Design a notification system”

Interviewers love this question because it tests interface design. Here’s how to approach it:

# Step 1: Identify the interface
class NotificationSender(ABC):
    @abstractmethod
    def send(self, recipient: str, message: str) -> bool:
        pass

# Step 2: Implement concrete classes
class EmailSender(NotificationSender):
    def send(self, recipient: str, message: str) -> bool:
        print(f"Sending email to {recipient}: {message}")
        return True

class SMSSender(NotificationSender):
    def send(self, recipient: str, message: str) -> bool:
        print(f"Sending SMS to {recipient}: {message}")
        return True

# Step 3: Use the interface in your system
class NotificationService:
    def __init__(self, senders: list[NotificationSender]):
        self.senders = senders
    
    def notify_all(self, recipient: str, message: str):
        for sender in self.senders:
            sender.send(recipient, message)

Talk through your design: “I’d start with a NotificationSender interface that defines a send method. Then I’d implement it for email, SMS, push notifications, etc. The notification service depends on the interface, so adding new notification types doesn’t require changing existing code.”


**4. Code Review Question: “What’s wrong with this code?”

Be ready to spot interface anti-patterns:

# What's wrong here?
class Animal(ABC):
    @abstractmethod
    def make_sound(self): pass
    
    @abstractmethod
    def lay_eggs(self): pass  # Not all animals lay eggs!

Your answer: “This violates the Interface Segregation Principle. Not all animals lay eggs, so a Dog class would have to implement a method it doesn’t need. I’d split this into Animal and EggLaying interfaces.”


**5. Practical Coding Challenge

You might be asked to implement a simple interface-based system. Practice this pattern:

# "Implement a caching system with multiple backends"

class Cache(ABC):
    @abstractmethod
    def get(self, key: str) -> Optional[str]: pass
    
    @abstractmethod
    def set(self, key: str, value: str, ttl: int = 0): pass

class MemoryCache(Cache):
    def __init__(self):
        self._data = {}
    
    def get(self, key: str) -> Optional[str]:
        return self._data.get(key)
    
    def set(self, key: str, value: str, ttl: int = 0):
        self._data[key] = value

class RedisCache(Cache):
    def get(self, key: str) -> Optional[str]:
        # Simulate Redis call
        return None
    
    def set(self, key: str, value: str, ttl: int = 0):
        # Simulate Redis call
        pass

Key points to mention:

  • Start with the interface
  • Keep methods focused and minimal
  • Explain how this makes testing easier
  • Mention you could add a FileCache or DatabaseCache without changing client code

**6. Follow-up Questions to Expect

  • “How would you test code that depends on this interface?” → Talk about mocking
  • “What if you need to add a new method to the interface?” → Discuss backward compatibility and versioning
  • “Can you give a real-world example from your experience?” → Prepare 1-2 examples beforehand

Red Flags to Avoid:

  • Saying “interfaces are just for Java” (they’re a universal concept)
  • Not being able to explain the benefits beyond “it’s good practice”
  • Designing interfaces that are too large or too small
  • Not mentioning testability as a key benefit

Key Takeaways

  • Interfaces define contracts that specify what methods a class must implement without dictating how. They answer “what can this do?” not “what is this?”

  • Enable polymorphism and loose coupling by letting code depend on abstractions rather than concrete implementations. This makes systems more flexible, testable, and extensible.

  • Keep interfaces small and focused (Interface Segregation Principle). Multiple small interfaces are better than one large interface that forces classes to implement methods they don’t need.

  • Use interfaces for dependency injection to make code testable. Instead of hard-coding dependencies, inject them through constructors or methods, depending on the interface type.

  • Python uses Abstract Base Classes (ABC) to achieve interfaces, while Java/C++ have explicit interface keywords. The concept is the same across languages: define behavior contracts without implementation.