The Adapter pattern — making incompatible interfaces work together

When your code expects one interface and a library gives you a different one, an Adapter is the thin wrapper that translates between them — without touching either side's actual code.

Intermediate

4 min read

The problem: two pieces of code that should work together, but don't

class PaymentProcessor:
    def process_payment(self, amount):
        ...   # your code calls this exact method name everywhere
 
class LegacyBillingSystem:
    def charge_customer(self, dollars):
        ...   # a third-party or legacy library, with a different method name entirely

Your application is written against a PaymentProcessor interface — every call site does processor.process_payment(amount). A new payment provider's library, LegacyBillingSystem, does the exact same underlying thing, but calls its method charge_customer instead. You can't edit the third-party library's source code, and rewriting every call site in your own codebase to match its specific naming would tie your application directly to one vendor's API — a real problem if you ever need to switch providers again.

The fix: a thin wrapper that translates between the two interfaces

class LegacyBillingAdapter:
    def __init__(self, legacy_system):
        self._legacy_system = legacy_system
 
    def process_payment(self, amount):
        self._legacy_system.charge_customer(amount)   # translates the call
 
legacy = LegacyBillingSystem()
processor = LegacyBillingAdapter(legacy)
processor.process_payment(50)   # your code never knows LegacyBillingSystem exists

LegacyBillingAdapter implements the interface your code already expects (process_payment), and internally translates each call into whatever the wrapped object actually needs (charge_customer). Every existing call site in your application keeps working completely unchanged — as far as they're concerned, they're talking to a normal PaymentProcessor. Neither PaymentProcessor's expected interface nor LegacyBillingSystem's actual implementation needs to be touched at all; the adapter is the only new code.

Why this is genuinely different from just editing one side

# Option A: edit LegacyBillingSystem directly — often impossible
# (third-party library, or a legacy system you can't safely modify)
 
# Option B: edit every call site to use charge_customer instead
# — ties your entire codebase to this one vendor's specific naming
 
# Option C: Adapter — translate in exactly one place

The two "obvious" fixes both have real problems: you frequently can't edit a third-party library's source at all, and even when you technically could, changing every call site to match one specific vendor's API directly couples your whole application to that vendor — switching providers later means touching every one of those call sites again. An adapter isolates the vendor-specific translation to exactly one place; swapping providers later means writing one new adapter class, with zero changes anywhere else in the application.

The concrete signal: "these two things do the same job, but speak different languages"

class StripeAdapter:
    def __init__(self, stripe_client):
        self._client = stripe_client
    def process_payment(self, amount):
        self._client.create_charge(amount_cents=amount * 100)
 
class PaypalAdapter:
    def __init__(self, paypal_client):
        self._client = paypal_client
    def process_payment(self, amount):
        self._client.send_payment(amount, currency="USD")

This is where Adapter's real value shows up: your application code can swap between StripeAdapter and PaypalAdapter — or add a third provider later — without a single change anywhere except the adapter itself and whichever line constructs it, because every adapter presents the exact same process_payment interface regardless of how differently the underlying vendor library actually works. Notice this also composes naturally with the Strategy pattern from its own lesson: once every payment provider is wrapped behind the same process_payment interface, choosing between them at runtime is exactly the Strategy pattern, operating on adapters instead of hand-written strategy classes.

How this differs from Decorator, since both "wrap" something

The Decorator pattern (from its own lesson) wraps an object to add behavior while keeping the same interface the wrapped object already had — a SugarDecorator still exposes .cost() and .description(), just as Coffee did, adding to what's there. Adapter wraps an object specifically to translate between two different interfaces — the wrapped object's actual interface (charge_customer) is different from what the adapter exposes (process_payment); nothing is being added, one interface is being made to look like another. Both patterns share the "wrap and delegate" mechanical shape, but they solve genuinely different problems: Decorator is about extending behavior, Adapter is about compatibility.

The concrete signal an Adapter belongs somewhere

Two pieces of code (yours and a library's, or two libraries) that do conceptually the same job but expose different method names, different argument shapes, or different data formats — and neither side can or should be rewritten to match the other — is the tell. If you own both sides and could just rename one to match the other for free, an adapter is unnecessary ceremony; it earns its keep specifically when one side is genuinely fixed (third-party, legacy, or shared with other consumers you don't control).

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Why is rewriting every call site to use LegacyBillingSystem's charge_customer method directly a worse fix than writing an adapter?

2. What does LegacyBillingAdapter.process_payment(amount) actually do?

3. What's the structural difference between Adapter and Decorator, given both wrap another object?

4. How does wrapping several payment providers behind their own adapters enable using the Strategy pattern to choose between them?