You have a clean interface your code depends on. Then you must integrate a legacy system or a third-party SDK whose method names, parameter types and data formats are all different, and you can't change it. Adapter puts a small translator class in between, so neither side has to change.
Intent#
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces. (GoF)
Analogy: a travel power adapter. Your US charger doesn't fit a European socket. The adapter changes neither the charger nor the socket; it just makes them fit.
The problem: SDK details leak into business logic#
Without an adapter, the service has to know every logger's API:
class StandardLogger:
def log(self, message: str) -> None:
print(f"[LOG] {message}")
# Third-party SDK: you cannot change this code
class ExternalDatadogSDK:
def send_event(self, payload: dict, priority: int) -> None:
print(f"Datadog received: {payload['msg']} with priority {priority}")
class OrderService:
def __init__(self, logger):
self.logger = logger
def create_order(self) -> None:
# Business logic now knows about every logging vendor
if isinstance(self.logger, StandardLogger):
self.logger.log("Order created")
elif isinstance(self.logger, ExternalDatadogSDK):
self.logger.send_event({"msg": "Order created"}, priority=1)
- Violates Open/Closed: switching to another vendor means another
elifinOrderServiceand every other service. - Violates Dependency Inversion: your core logic should set the rules, not reshape dictionaries for an external tool.
Structure#
An adapter sits between the client and the class it can't call directly.
| Participant | Role |
|---|---|
| Target | The interface your client expects |
| Adaptee | The existing class with a useful but incompatible interface |
| Adapter | Implements Target, holds an Adaptee, translates each call "under the hood" |
| Client | Uses only Target; doesn't know an adapter is involved |
Two flavors:
- Object adapter (composition): the adapter has an adaptee. The usual choice: it works with any subclass of the adaptee.
- Class adapter (inheritance): the adapter is an adaptee and also implements the target. In Python this is multiple inheritance (
class Adapter(Adaptee, Target)); languages with single class inheritance only allow it when the target is an interface.
Example 1: logging SDK#
The service logs through one interface; the adapter translates each call for the vendor SDK.
from abc import ABC, abstractmethod
# Target: what your system expects
class LoggerInterface(ABC):
@abstractmethod
def log(self, message: str) -> None: ...
class StandardLogger(LoggerInterface):
def log(self, message: str) -> None:
print(f"[LOG] {message}")
# Adaptee: the external SDK, unchanged
class ExternalDatadogSDK:
def send_event(self, payload: dict, priority: int) -> None:
print(f"Datadog received: {payload['msg']} with priority {priority}")
# Adapter: implements YOUR interface, translates to THEIR API
class DatadogAdapter(LoggerInterface):
def __init__(self, sdk: ExternalDatadogSDK):
self._sdk = sdk
def log(self, message: str) -> None:
self._sdk.send_event(payload={"msg": message}, priority=1)
class OrderService:
# Depends only on the abstraction
def __init__(self, logger: LoggerInterface):
self.logger = logger
def create_order(self) -> None:
self.logger.log("Order created successfully")
OrderService(StandardLogger()).create_order()
# [LOG] Order created successfully
OrderService(DatadogAdapter(ExternalDatadogSDK())).create_order()
# Datadog received: Order created successfully with priority 1
The adapter acts as an anti-corruption layer. If the vendor renames send_event() next year, you change one adapter file and nothing else notices.
Example 2: payment gateways#
Adapters often convert types and return values, not just method names.
from abc import ABC, abstractmethod
# Target
class PaymentGateway(ABC):
@abstractmethod
def process_payment(self, amount: float) -> None: ...
@abstractmethod
def refund(self, amount: float, transaction_id: str) -> bool: ...
@abstractmethod
def get_payment_status(self, transaction_id: str) -> str: ...
# Adaptee 1: legacy system with different names and types
class LegacyPaymentSystem:
def make_payment(self, amount: str) -> None:
print(f"Legacy: paying ${amount}")
def cancel_transaction(self, txn_id: str, amount: str) -> None:
print(f"Legacy: cancelling {txn_id} for ${amount}")
def check_status(self, txn_id: str) -> int:
return 1 # 1 = success, 0 = pending, -1 = failed
# Adaptee 2: third-party library that works in integer cents and int IDs
class ThirdPartyPayPal:
def pay(self, cents: int) -> None:
print(f"PayPal: charging {cents} cents")
def do_refund(self, cents: int, trans_id: int) -> str:
return "REFUNDED"
def get_transaction_details(self, trans_id: int) -> str:
return "COMPLETED"
class LegacyPaymentAdapter(PaymentGateway):
def __init__(self, legacy: LegacyPaymentSystem):
self._legacy = legacy
def process_payment(self, amount: float) -> None:
self._legacy.make_payment(str(amount)) # float → str
def refund(self, amount: float, transaction_id: str) -> bool:
self._legacy.cancel_transaction(transaction_id, str(amount))
return True
def get_payment_status(self, transaction_id: str) -> str:
# int code → status string
match self._legacy.check_status(transaction_id):
case 1:
return "SUCCESS"
case 0:
return "PENDING"
case -1:
return "FAILED"
case _:
return "UNKNOWN"
class PayPalAdapter(PaymentGateway):
def __init__(self, paypal: ThirdPartyPayPal):
self._paypal = paypal
def process_payment(self, amount: float) -> None:
self._paypal.pay(round(amount * 100)) # dollars → int cents
def refund(self, amount: float, transaction_id: str) -> bool:
try:
txn = int(transaction_id) # str ID → int ID
except ValueError:
return False
result = self._paypal.do_refund(round(amount * 100), txn)
return result == "REFUNDED"
def get_payment_status(self, transaction_id: str) -> str:
try:
txn = int(transaction_id)
except ValueError:
return "INVALID_ID"
return self._paypal.get_transaction_details(txn)
# Client: works with any gateway, unaware of adapters
class PaymentProcessor:
def __init__(self, gateway: PaymentGateway):
self._gateway = gateway
def process_order(self, amount: float, order_id: str) -> None:
self._gateway.process_payment(amount)
if self._gateway.get_payment_status(order_id) == "FAILED":
self._gateway.refund(amount, order_id)
legacy = LegacyPaymentAdapter(LegacyPaymentSystem())
PaymentProcessor(legacy).process_order(100.50, "ORDER001")
paypal = PayPalAdapter(ThirdPartyPayPal())
PaymentProcessor(paypal).process_order(250.75, "12345")
Adapters can also convert units and semantics. A legacy rectangle whose scale() takes a percentage can sit behind a Shape.resize(factor) interface:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def draw(self) -> None: ...
@abstractmethod
def resize(self, factor: float) -> None: ...
@abstractmethod
def get_area(self) -> float: ...
class LegacyRectangle:
def __init__(self, w: float, h: float):
self.w, self.h = w, h
def display(self) -> None:
print(f"Rectangle {self.w} x {self.h}")
def scale(self, percent: float) -> None:
self.w *= percent / 100
self.h *= percent / 100
def calculate_area(self) -> float:
return self.w * self.h
class RectangleAdapter(Shape):
def __init__(self, rect: LegacyRectangle):
self._rect = rect
def draw(self) -> None:
self._rect.display() # display() → draw()
def resize(self, factor: float) -> None:
self._rect.scale(factor * 100) # factor → percentage
def get_area(self) -> float:
return self._rect.calculate_area()
shape = RectangleAdapter(LegacyRectangle(2, 3))
shape.resize(2)
print(shape.get_area()) # 24.0
Example 3: class adapter and two-way adapter#
A class adapter inherits from the adaptee instead of wrapping it:
from abc import ABC, abstractmethod
class ModernPrinter(ABC):
@abstractmethod
def print_text(self, text: str) -> None: ...
class OldPrinter:
def print_black_and_white(self, content: str) -> None:
print(f"B&W: {content}")
# Class adapter: inherits the adaptee, implements the target
class PrinterClassAdapter(OldPrinter, ModernPrinter):
def print_text(self, text: str) -> None:
# Inherited method, no wrapped field
self.print_black_and_white(text)
PrinterClassAdapter().print_text("hello") # B&W: hello
A two-way adapter implements both interfaces so it can be used from either side:
from abc import ABC, abstractmethod
class MediaPlayer(ABC):
@abstractmethod
def play(self, filename: str) -> None: ...
@abstractmethod
def stop(self) -> None: ...
class AudioDevice(ABC):
@abstractmethod
def start_playback(self, file: str) -> None: ...
@abstractmethod
def stop_playback(self) -> None: ...
@abstractmethod
def pause(self) -> None: ...
class TwoWayAdapter(MediaPlayer, AudioDevice):
# MediaPlayer side: delegates to the AudioDevice methods
def play(self, filename: str) -> None:
self.start_playback(filename)
def stop(self) -> None:
self.stop_playback()
# AudioDevice side
def start_playback(self, file: str) -> None:
print(f"Playing: {file}")
def stop_playback(self) -> None:
print("Stopped")
def pause(self) -> None:
print("Paused")
TwoWayAdapter().play("song.mp3") # Playing: song.mp3
Template#
This is the bare object-adapter shape to start from:
from abc import ABC, abstractmethod
class Target(ABC):
@abstractmethod
def request(self) -> None: ...
class Adaptee:
def specific_request(self) -> None:
"""Useful, but the wrong shape."""
class Adapter(Target):
def __init__(self, adaptee: Adaptee):
self._adaptee = adaptee
def request(self) -> None:
self._adaptee.specific_request() # translate, then delegate
Real-world examples#
You already use adapters in the standard library and in most integrations:
io.TextIOWrapperadapts a binary stream into a text stream.- Python DB-API drivers (
sqlite3,psycopg) adapt each database's native protocol to one common connection-and-cursor interface. - Payment processors (Stripe, PayPal, a legacy bank API) behind one gateway interface.
- SMS providers like Twilio behind a notifier interface.
- Import/export of CSV, JSON and XML behind one reader interface.
When to use it#
Adapter fits when the mismatch is in the interface, not the behaviour:
- You want to use an existing class whose interface doesn't match.
- You're integrating third-party or legacy code you can't modify.
- You want several implementations behind one common interface.
- You're migrating systems or supporting old and new API versions side by side.
When not to use it#
Skip it in these cases:
- You own the source and can simply change the class.
- The interfaces are fundamentally different in what they do, not just how they're named. No thin translator will fix that.
- You'd need adapters everywhere, which signals a redesign is due.
Pitfalls#
Keep adapters thin and watch for these problems:
- Lossy translation. Converting dollars to integer cents or string IDs to
intcan lose precision or fail; handle it explicitly, as the PayPal adapter does. - Business logic creeping into adapters. Keep them to translation and delegation.
- Extra layer to debug, and a small cost for each extra call.
- Adapter sprawl if every integration grows its own slightly different adapter.
Adapter vs similar patterns#
Several structural patterns wrap an object. They differ in what they do to its interface:
| Pattern | Interface | Purpose |
|---|---|---|
| Adapter | Changes it | Make an existing class fit |
| Decorator | Keeps it | Add behavior |
| Proxy | Keeps it | Control access |
| Facade | New, simpler one | Simplify a whole subsystem |
| Bridge | Designed up front | Let abstraction and implementation vary independently |
Adapter is applied after the fact to make existing things fit; Bridge is designed up front.
Key takeaways#
- Adapter implements the interface you want and translates calls to the class you have.
- Prefer the object adapter (composition); a class adapter needs inheritance from the adaptee.
- It's your anti-corruption layer: vendor changes stay inside one class.
- Adapters translate names, types, units and return codes. Keep business logic out of them.