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.
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:
By the end, you’ll see why the Adapter pattern centralizes conversion logic and reduces code duplication.
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:
FastPay API returns JSON like this:
{
"customer": { "id": "abc123" },
"transaction": { "total_amount": "99.99", "currency": "EUR" }
}
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.
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:
get_payment_info(transaction_id) -> dict
).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
.get_payment_info(transaction_id)
so internal billing logic can use either interchangeably.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
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.
BillingService
or existing client code.By using the Adapter pattern, you maintain a clean, modular, and easily testable codebase even as you integrate multiple external APIs.