Loading episodes…
0:00 0:00

The Adapter Pattern in Python: A Visual Guide to Connecting Incompatible Interfaces

00:00
BACK TO HOME

The Adapter Pattern in Python: A Visual Guide to Connecting Incompatible Interfaces

10xTeam November 21, 2025 11 min read

You’ve built a perfectly good system, but now you need to integrate a new, third-party component. The problem? The new component speaks a completely different language—its methods, parameters, and return types are totally incompatible with your existing code. Do you rewrite your entire system? No! You use the Adapter Pattern.

The Adapter is a structural design pattern that acts as a translator, allowing objects with incompatible interfaces to work together seamlessly.

Think of a universal travel adapter. Your laptop charger has a two-prong plug (your existing system), but the country you’re visiting has three-prong wall outlets (the new service). The adapter sits in the middle, making the connection possible without you having to change your charger or the wall outlet.

The Problem in Code: Data Transformation

Imagine you have a data provider that gives you data in XML format, but your application’s analytics library only understands JSON. The interfaces are incompatible.

An adapter can be created to sit between them, converting the XML data into JSON on the fly.

graph TD;
    A[Data Provider <br/>(Returns XML)] --> B{XML-to-JSON Adapter};
    B --> C[Analytics Library <br/>(Expects JSON)];
    subgraph Your Application
        B
        C
    end

This adapter makes the Analytics Library believe it’s communicating with a native JSON source, completely unaware of the XML-to-JSON translation happening behind the scenes.

A Real-World Example: Payment Gateways

Let’s build a more concrete example. We have a CheckoutService in our e-commerce application. Initially, it was built to only support PayPal for payments. The entire system is built around PayPal’s API, which takes an amount and currency, and returns a simple boolean (True for success, False for failure).

Now, the business wants to add Stripe as a new payment option. Here’s the catch: Stripe’s API is different.

  • It requires an additional description field in the request.
  • Its success response isn’t a boolean; it’s a dictionary like {'status': 'approved'}.

Our mobile app and other third-party clients are already using our system. They all expect a boolean response. We can’t force them all to update their code just to accommodate Stripe’s unique response format. This is where the Adapter pattern shines.

Here is the class structure we want to achieve.

classDiagram
    class Client {
        +checkout(processor)
    }
    class PaymentProcessor {
        <<interface>>
        +pay(amount, currency) bool
    }
    class PayPalProcessor {
        +pay(amount, currency) bool
    }
    class StripeService {
        <<adaptee>>
        -process_payment(charge_details) dict
    }
    class StripeAdapter {
        -stripe_service: StripeService
        +pay(amount, currency) bool
    }

    Client --> PaymentProcessor
    PaymentProcessor <|-- PayPalProcessor
    PaymentProcessor <|-- StripeAdapter
    StripeAdapter o-- StripeService
  • Target (PaymentProcessor): The interface our client code depends on.
  • Adaptee (StripeService): The incompatible class we need to integrate.
  • Adapter (StripeAdapter): The class that bridges the gap between the Target and the Adaptee.
  • Client: The code that uses the payment system.

Project Structure

To keep our code organized, we’ll structure our project as follows. The adapters for new services will live in their own dedicated directory.

payment_project/
├── main.py
├── checkout.py
├── processors/
│   ├── __init__.py
│   ├── base.py
│   └── paypal_processor.py
└── adapters/
    ├── __init__.py
    └── stripe_adapter.py

Step 1: The Initial System (PayPal-Only)

First, let’s define our Target interface using Python’s abc module. This is the contract all payment processors must follow.

processors/base.py

from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    """
    The Target Interface that our client code uses.
    """
    @abstractmethod
    def pay(self, amount: float, currency: str = "USD") -> bool:
        pass

Next, our original PayPalProcessor implements this interface.

processors/paypal_processor.py

from .base import PaymentProcessor
import time

class PayPalProcessor(PaymentProcessor):
    """
    A Concrete Implementation for the PayPal service.
    """
    def pay(self, amount: float, currency: str = "USD") -> bool:
        print(f"Processing ${amount} payment via PayPal...")
        # Simulate API call to PayPal
        time.sleep(1)
        print("PayPal payment successful.")
        return True

Finally, our CheckoutService uses the PaymentProcessor abstraction, not a concrete implementation. This is a key principle (Dependency Inversion) that makes our system flexible.

checkout.py

from processors.base import PaymentProcessor

class CheckoutService:
    def __init__(self, processor: PaymentProcessor):
        self._processor = processor

    def execute_payment(self, amount: float, currency: str = "USD"):
        print("--- Checkout Service ---")
        if self._processor.pay(amount, currency):
            print("Payment Succeeded.")
        else:
            print("Payment Failed.")
        print("------------------------")

Step 2: The Incompatible Service (Stripe)

Now, let’s define the “incompatible” Stripe service. Notice its process_payment method has a different signature and return type. We’ll simulate it in a file that our adapter will use.

adapters/stripe_service.py (Simulated Third-Party Library)

import time

class StripeService:
    """
    The Adaptee: An incompatible third-party service.
    It has a different method name, parameters, and return type.
    """
    def process_payment(self, charge_details: dict):
        print(f"Processing payment for {charge_details['amount']} via Stripe...")
        # Simulate API call to Stripe
        time.sleep(1)
        if charge_details.get("amount", 0) > 0:
            return {"status": "approved", "transaction_id": "xyz-123"}
        else:
            return {"status": "declined", "reason": "invalid_amount"}

[!NOTE] The StripeService is our Adaptee. This is the class with the incompatible interface that we want to adapt to our system. We cannot (or should not) modify its source code.

Step 3: Building the Stripe Adapter

Here comes the magic. We create StripeAdapter, which inherits from our PaymentProcessor (the Target) but internally wraps and uses the StripeService (the Adaptee).

adapters/stripe_adapter.py

from processors.base import PaymentProcessor
from .stripe_service import StripeService # The incompatible service

class StripeAdapter(PaymentProcessor):
    """
    The Adapter makes the StripeService's interface compatible with
    the PaymentProcessor's interface.
    """
    def __init__(self):
        self._stripe_service = StripeService()

    def pay(self, amount: float, currency: str = "USD") -> bool:
        # 1. Translate the request from our system's format to Stripe's format
        print("--> Stripe Adapter: Translating request...")
        stripe_request = {
            "amount": amount,
            "currency": currency,
            "description": "Checkout from Adapter Demo"
        }

        # 2. Call the adaptee's method
        response = self._stripe_service.process_payment(stripe_request)

        # 3. Translate the response from Stripe's format to our system's format (bool)
        print("--> Stripe Adapter: Translating response...")
        if response.get("status") == "approved":
            return True
        return False

The adapter performs a two-way translation:

  1. Request Translation: It converts the simple (amount, currency) call into the dictionary that StripeService expects.
  2. Response Translation: It converts the {'status': 'approved'} dictionary from Stripe into the simple True/False boolean that our CheckoutService expects.

Step 4: Updating the Client Code

Thanks to our adapter, the changes required in the main application logic are minimal. We just need to add logic to select the correct processor. The CheckoutService itself remains completely unchanged!

main.py

  from checkout import CheckoutService
  from processors.paypal_processor import PayPalProcessor
+ from adapters.stripe_adapter import StripeAdapter

  def main():
      gateway = input("Choose payment gateway (paypal or stripe): ").lower()
      amount = float(input("Enter amount to pay: "))

      processor = None
      if gateway == "paypal":
          processor = PayPalProcessor()
+     elif gateway == "stripe":
+         processor = StripeAdapter()
      else:
          print("Invalid gateway selected.")
          return

      if processor:
          checkout_service = CheckoutService(processor)
          checkout_service.execute_payment(amount)

  if __name__ == "__main__":
      main()

Now, when we run the application, we can seamlessly switch between PayPal and Stripe, and our CheckoutService is none the wiser. It continues to work with the PaymentProcessor interface, just as before.

[!TIP] Code Against Abstractions, Not Concretions. The power of this pattern comes from the CheckoutService depending on the PaymentProcessor abstract class, not the concrete PayPalProcessor. This allows us to swap in any new processor (like our StripeAdapter) that adheres to the same interface without modifying the service.


Deep Dive: Object vs. Class Adapters
There are two main implementations of the Adapter pattern: 1. **Object Adapter (Composition):** This is the version we implemented. The `Adapter` contains an instance of the `Adaptee` and delegates calls to it. This is the most common and flexible approach in Python, as it allows you to adapt a class and all of its subclasses. 2. **Class Adapter (Inheritance):** This version uses multiple inheritance. The `Adapter` inherits from both the `Target` interface and the `Adaptee` class. This is less common in Python because it tightly couples the adapter to a specific adaptee class. You can't adapt a class *and* its subclasses. The Object Adapter approach using composition is generally preferred for its flexibility.


When to Use the Adapter Pattern

  • When you need to use an existing class, but its interface is not compatible with the rest of your code.
  • When you want to create a reusable class that cooperates with unrelated or unforeseen classes that don’t necessarily have compatible interfaces.
  • When you need to integrate a third-party library or legacy component without changing its source code.

Pros and Cons

Pros Cons
Single Responsibility Principle: You separate the integration logic from your core business logic. Increased Complexity: You introduce a new layer of objects that have to be managed.
Open/Closed Principle: You can introduce new adapters without modifying the existing client code. Potential for Over-engineering: For very simple translations, a direct function call might be easier.
Improved Reusability: Adapters can be reused to integrate the same service in different parts of an application. “Adapter Hell”: In complex systems, you can end up with chains of adapters adapting other adapters.

Final Summary

The Adapter pattern is an essential tool for building flexible and maintainable systems. It allows you to integrate new and legacy components gracefully, acting as the glue that holds incompatible parts together.

mindmap
  root((Adapter Pattern))
    Definition
      :A structural pattern that allows incompatible interfaces to work together.
    Analogy
      :A universal travel adapter.
    Participants
      Target
        :The interface the client expects.
      Adaptee
        :The incompatible service/class.
      Adapter
        :The wrapper that translates between Target and Adaptee.
      Client
        :The code that uses the Target interface.
    Benefits
      Single Responsibility
      Open/Closed Principle
      Integrates Legacy/3rd-Party Code
    Implementation (Python)
      Use Composition (Object Adapter)
      Inherit from an Abstract Base Class (Target)
      Wrap an instance of the Adaptee


🧠 Quiz: Test Your Knowledge!
  1. In our example, which class is the "Adaptee"?
  2. Why is it better for `CheckoutService` to depend on `PaymentProcessor` instead of `PayPalProcessor` directly?
  3. What is the primary responsibility of the `StripeAdapter`?

View Answers
  1. The `StripeService` class is the Adaptee. It's the class with the incompatible interface we want to use.
  2. By depending on the `PaymentProcessor` abstraction (an ABC), the `CheckoutService` is decoupled from any specific implementation. This allows us to introduce new payment methods (like Stripe via its adapter) without changing the `CheckoutService`'s code, adhering to the Open/Closed Principle.
  3. The `StripeAdapter`'s primary responsibility is to perform a two-way translation: it translates method calls from the client's expected interface (`PaymentProcessor`) into calls the `StripeService` can understand, and then translates the `StripeService`'s response back into the format the client expects.

Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?