Adapter Pattern in Python: Keeping Your Code Clean and Maintainable

February 21, 2024 in Python, Design Patterns, Tutorial by Rakan Farhouda5 minutes

Learn how the Adapter pattern works in Python, with a real-world example to keep your code clean and maintainable.

Adapter Pattern in Python: Keeping Your Code Clean and Maintainable

When you integrate multiple external services, their interfaces often don’t match the data structures your internal code expects. The Adapter pattern helps by creating a translation layer between these incompatible interfaces. In this post, we’ll:

  1. Define a problem: two payment APIs with different response formats.
  2. Implement two separate adapters, each wrapping its own API client.
  3. Show how client code can use a common interface to process payments from either API without changes.

By the end, you’ll see why the Adapter pattern centralizes conversion logic and reduces code duplication.


The Problem: Multiple Payment APIs with Incompatible Interfaces

Imagine you have an internal billing service that expects payment information in this format:

{
    'user_id': str,
    'amount': float,
    'currency': str,
}

Your internal code uses a method like:

def charge_customer(payment_info):
    # payment_info is a dict with keys: user_id, amount, currency
    # ...existing logic to charge the user...
    pass

Now you need to integrate with two different payment providers:

  1. FastPay API returns JSON like this:

    {
      "customer": { "id": "abc123" },
      "transaction": { "total_amount": "99.99", "currency": "EUR" }
    }
  2. EasyCharge API returns JSON in a different shape:

    {
      "uid": "user_456",
      "payment": { "amount_cents": 1500, "currency_code": "USD" }
    }

Without adapters, you might sprinkle conversion code throughout your billing logic:

# Scattered conversion for FastPay:
def process_fastpay(transaction_id):
    raw = fastpay_client.fetch(transaction_id)
    payment_info = {
        'user_id': raw['customer']['id'],
        'amount': float(raw['transaction']['total_amount']),
        'currency': raw['transaction']['currency'],
    }
    return charge_customer(payment_info)

# Scattered conversion for EasyCharge:
def process_easycharge(txn_id):
    raw = easycharge_client.get(txn_id)
    payment_info = {
        'user_id': raw['uid'],
        'amount': raw['payment']['amount_cents'] / 100,
        'currency': raw['payment']['currency_code'],
    }
    return charge_customer(payment_info)

This approach duplicates conversion logic and couples billing code to each API format. If either format changes, you update many places.


Pattern Overview: Centralize Conversion with Adapters

The Adapter pattern introduces a new class that implements a common target interface while wrapping the third-party client (the Adaptee). Each Adapter handles conversion internally. Client code depends only on the Adapter’s interface, not on raw API responses.

Key components:

  • Target Interface: The internal format (get_payment_info(transaction_id) -> dict).
  • Adaptee Interface: The raw methods exposed by each payment client.
  • Adapter: Implements get_payment_info(...), calls the Adaptee’s fetch method, then transforms the response to the internal format.

We’ll implement:

  • FastPayAdapter wraps FastPayClient.
  • EasyChargeAdapter wraps EasyChargeClient.
  • Both adapters provide get_payment_info(transaction_id) so internal billing logic can use either interchangeably.

Code: Implementing Two Adapters

Below is full code for each payment client, the adapters, and a unified billing service that uses them without caring which API is behind the scenes.

# fastpay_client.py (existing third-party client)
import requests

class FastPayClient:
    def __init__(self, api_key: str):
        self.api_key = api_key

    def fetch(self, transaction_id: str) -> dict:
        url = f"https://api.fastpay.com/txn/{transaction_id}?key={self.api_key}"
        response = requests.get(url)
        return response.json()
# easycharge_client.py (existing third-party client)
import requests

class EasyChargeClient:
    def __init__(self, token: str):
        self.token = token

    def get(self, transaction_id: str) -> dict:
        url = f"https://api.easycharge.io/payments/{transaction_id}"
        headers = { 'Authorization': f"Bearer {self.token}" }
        response = requests.get(url, headers=headers)
        return response.json()
# adapters.py (our Adapters)
from fastpay_client import FastPayClient
from easycharge_client import EasyChargeClient

class FastPayAdapter:
    """
    Adapter for FastPay API: converts FastPay response to internal format.
    """
    def __init__(self, api_key: str):
        self.client = FastPayClient(api_key)

    def get_payment_info(self, transaction_id: str) -> dict:
        raw = self.client.fetch(transaction_id)
        # FastPay: {'customer': {'id': 'abc123'}, 'transaction': {'total_amount': '99.99', 'currency': 'EUR'}}
        return {
            'user_id': raw['customer']['id'],
            'amount': float(raw['transaction']['total_amount']),
            'currency': raw['transaction']['currency'],
        }

class EasyChargeAdapter:
    """
    Adapter for EasyCharge API: converts EasyCharge response to internal format.
    """
    def __init__(self, token: str):
        self.client = EasyChargeClient(token)

    def get_payment_info(self, transaction_id: str) -> dict:
        raw = self.client.get(transaction_id)
        # EasyCharge: {'uid': 'user_456', 'payment': {'amount_cents': 1500, 'currency_code': 'USD'}}
        return {
            'user_id': raw['uid'],
            'amount': raw['payment']['amount_cents'] / 100.0,
            'currency': raw['payment']['currency_code'],
        }
# billing_service.py (internal code)
from adapters import FastPayAdapter, EasyChargeAdapter

class BillingService:
    """
    Unified billing service that can process payments through any adapter.
    """
    def __init__(self, provider: str, credential: str):
        # Choose adapter based on provider name
        if provider == 'fastpay':
            self.adapter = FastPayAdapter(credential)
        elif provider == 'easycharge':
            self.adapter = EasyChargeAdapter(credential)
        else:
            raise ValueError(f"Unknown provider: {provider}")

    def charge(self, transaction_id: str) -> bool:
        # Use the adapter to get internal payment_info, then charge
        payment_info = self.adapter.get_payment_info(transaction_id)
        return self._charge_customer(payment_info)

    def _charge_customer(self, payment_info: dict) -> bool:
        # Existing internal logic expects: {'user_id', 'amount', 'currency'}
        print(f"Charging {payment_info['user_id']} {payment_info['amount']} {payment_info['currency']}")
        # Implement actual charging logic here...
        return True

Usage: How to Use Adapters Interchangeably

With this setup, your code that processes payments doesn’t need to know which API you’re calling. For example:

# main.py
from billing_service import BillingService

# Process a FastPay transaction
fastpay_service = BillingService(provider='fastpay', credential='FASTPAY_API_KEY')
fastpay_service.charge('txn_123')

# Process an EasyCharge transaction
easy_service = BillingService(provider='easycharge', credential='EASYCHARGE_TOKEN')
easy_service.charge('pay_789')

No matter if you call FastPay or EasyCharge, both adapters conform to the same interface (get_payment_info), so BillingService.charge() remains unchanged.


Lessons Learned & Tips

  • Centralize conversions: Keep all data mapping inside Adapter classes instead of scattering it across functions.
  • Decouple your code: Internal logic calls the Adapter’s interface, not the raw API.
  • Easily swap providers: Add a new adapter for another payment service without modifying BillingService or existing client code.
  • Follow SOLID principles: Adapter upholds Single Responsibility (converting one format) and Open/Closed (new adapters extend without changes to existing ones).

By using the Adapter pattern, you maintain a clean, modular, and easily testable codebase even as you integrate multiple external APIs.

Happy coding, Rakan Farhouda LinkedIn · GitHub