Course contents

Adapter — bridging incompatible interfaces

Sign in to run this lab and save your progress

Learn

Before you start

Adapter is a small pattern with a big role: it's the bridge between "code I own" and "code I don't". Almost every production system in the world uses it — usually without being called out by name, always doing the same job.

The one-line version

You have code that speaks interface A. You have to work with code that speaks interface B. Adapter is a class that IS-A "A" (from the caller's view) and HOLDS a "B" (which it delegates to inside).

When to use Adapter

Any time you see these signs:

  • Shape mismatch across a boundary. Your code's shape differs from what's on the other side of a library / API / legacy service.
  • Wrapping a third-party client. You want an interface YOUR app owns, so you can swap vendors later.
  • Fake versions for tests. You want to swap a real database / HTTP client with a fake one. Each hides behind a shared interface.
  • Multiple vendors, one shared contract. Twilio, MessageBird, and AWS SNS all send SMS — one SmsSender interface, one adapter per vendor.

Recognition trick: whenever a class in your code holds a reference to a "foreign" class and translates between them, you're probably looking at an Adapter.

Composition vs inheritance

Adapter has two flavors:

  • Class adapter — subclass the thing you're adapting. Design C in the objective. Java historically used this because it has real multiple inheritance.
  • Object adapter — hold the thing you're adapting as a field. Design D. Python's natural default.

Object adapter wins for Python because:

  1. Loose coupling. Adapter isn't tied to the whole API of the thing it wraps — just uses what it needs.
  2. Swappability. Multiple adapters for the same interface can each wrap a different thing.
  3. Testability. Faking the wrapped thing is easier when it's a field, not a parent class.

In this course, when we say "Adapter", assume object adapter unless we say otherwise.

Where Adapter shows up in design problems

  • Payment gateway wrapping (each gateway is different; a PaymentGatewayAdapter per vendor lets you swap them). Overlaps with Abstract Factory when you have multiple related adapters per family.
  • Storage backends — S3, GCS, local disk each wrapped behind a Storage interface.
  • Logging integrations — third-party logging services (Sentry, DataDog, Splunk) each with a different API wrapped behind an internal Logger interface.
  • Notification channels — SendGrid, Twilio, Slack each behind a Notifier interface.
  • Legacy database wrappers — old ORM's connection object wrapped to match a modern async interface.

Adapter vs Facade (previewing the next lesson)

Two patterns that both wrap other code. Different jobs:

  • Adapter — makes an incompatible thing look compatible. One-to-one shape translation.
  • Facade — hides a complex system behind a simple entry point. Many-to-one, and the underlying pieces don't need to be "wrong" in shape.

If you're translating shape, it's Adapter. If you're simplifying, it's Facade.

The interview cliché to avoid

Interviewers hate this answer: "It adapts the interface." Yes, we know. Give them what they actually want to hear:

  • WHY you'd use Adapter over other options (rewriting the vendor, subclassing, inlining)
  • WHERE you've used it in similar problems
  • The composition-over-inheritance choice and WHY

Specific answers show experience. Definitions show memorization.

The decision cheat-sheet

Situation Answer
Third-party API's shape doesn't match yours Adapter
Multiple vendors, same conceptual operation Adapter (per vendor) + a factory
One-off translation, one call site Just inline it
You control both sides Make the interfaces match, no adapter
Complex subsystem needs one simple entry point Facade (next lesson)
You want to add behavior AROUND an existing object Decorator (lesson 8)
You want a stand-in that controls access Proxy (lesson 9)

Adapter, Facade, Decorator, and Proxy all wrap other objects — this table is your recognition guide.

Your task

Adapter is the pattern for when your code needs to talk to a class with a DIFFERENT shape — and you can't (or shouldn't) change either side.

The problem

Your app has a clean WeatherService interface:

class WeatherService(ABC):
    @abstractmethod
    def current_temperature(self, city): ...
    @abstractmethod
    def five_day_forecast(self, city): ...

Every part of your app that shows weather uses this interface — the dashboard, the notification service, the trip planner.

Now your team buys a legacy weather library, LegacyMet, that has:

class LegacyMet:
    def get_temp_c(self, city_name): ...           # returns int
    def get_forecast(self, city_name, days): ...   # returns dict

Different method names. Different arguments. Different return types. But it's a working library with 20 years of bug fixes. Rewriting it is a 6-month project. Renaming every call site in your app to use its methods directly is a 3-week refactor and locks your code to this specific library forever.

Adapter is the fix.

Questions to ask about the requirements

  1. Do you control both sides? If yes, just make the interfaces match — no adapter needed. If no (third-party library, legacy code, old internal service), Adapter is on the table.
  2. How many places call the interface? One → maybe just inline the translation. Many → an adapter class pays off.
  3. Will you have MORE versions of the target interface later (LegacyMet today, ModernMet next year, an in-house version after that)? If yes, Adapter is definitely right — it keeps your callers independent of any one vendor.
  4. Does the legacy library differ only in method names, or also in SHAPE (return types, argument order, error handling)? Name-only differences are easy; shape differences are where Adapter really pays off.
  5. Is the target interface stable? If it changes often, the adapter changes with it. Nail the interface first.

For this problem: we don't control LegacyMet, many places call WeatherService, we plan to add ModernMet later, and the shapes differ. Adapter clearly fits.

Different designs, compared

Design A — inline the translation everywhere

def show_weather(city):
    legacy = LegacyMet()
    temp = legacy.get_temp_c(city)         # inline
    ...
  • Good: No new abstractions.
  • Bad: Every caller knows about LegacyMet directly. Switching to ModernMet later means touching every caller. Translation logic gets copied. Your code is tied to a library that might change or be replaced.
  • When it wins: One call site AND you'll never swap the library. Rare.

Design B — rename LegacyMet's methods

Modify LegacyMet so it has current_temperature etc.

  • Good: No adapter needed. Interfaces line up.
  • Bad: You often CAN'T — it's a third-party library, or changing it breaks other users. Even when you can, you own that maintenance forever. Doesn't scale to future libraries.
  • When it wins: You own the code, no other users, library is small. Rare in real systems.

Design C — subclass LegacyMet, add the new method names

class ModernMet(LegacyMet):
    def current_temperature(self, city):
        return self.get_temp_c(city)
  • Good: Gets LegacyMet's code for free. Adds new-name methods.
  • Bad: The object IS-A LegacyMet — inherits everything, including deprecated methods, weird internals, and the whole base API. Fragile in the LSP sense. Test doubles have to fake LegacyMet's methods too. Also: only works if LegacyMet doesn't already have methods with the names you want to add.
  • When it wins: Rarely. Inheritance-based adapters tempt beginners; production teams almost always use composition.

Design D — composition-based Adapter

class LegacyMetAdapter(WeatherService):
    def __init__(self, legacy):
        self._legacy = legacy    # HAS-A, not IS-A

    def current_temperature(self, city):
        return self._legacy.get_temp_c(city)

    def five_day_forecast(self, city):
        raw = self._legacy.get_forecast(city, 5)
        # translate raw shape into what callers expect
        return [
            {"day": i + 1, "high_c": raw["days"][i]["hi"],
             "low_c": raw["days"][i]["lo"]}
            for i in range(5)
        ]
  • Good: Adapter IS-A WeatherService (matches the target interface). Legacy library stays wrapped and hidden. Swap LegacyMet for ModernMet by writing a new adapter — zero caller changes. Test doubles are easy.
  • Bad: One extra class per legacy library. Small extra indirection at every call.
  • When it wins: Anywhere the interface has multiple versions OR the underlying library might get swapped.

The locked-in choice: Design D

Reviewing the requirements:

  • We don't control LegacyMet. → Rules out B.
  • Many places call WeatherService. → Rules out A.
  • We plan to add ModernMet next year. → Rules out A again, and C (inheritance locks us to LegacyMet).
  • Return shapes differ. → D's composition gives us a natural place for translation.

Design D wins. Both C and D solve the "different shape" problem, but D doesn't drag LegacyMet's whole API with it — which matters when we add ModernMet.

Mechanics

from abc import ABC, abstractmethod


class WeatherService(ABC):
    @abstractmethod
    def current_temperature(self, city): ...
    @abstractmethod
    def five_day_forecast(self, city): ...


# The legacy library (imagine you can't modify this).
class LegacyMet:
    def get_temp_c(self, city_name):
        return {"Mumbai": 30, "Delhi": 20, "London": 12}.get(city_name, 15)

    def get_forecast(self, city_name, days):
        # returns a dict shape we don't like
        base = self.get_temp_c(city_name)
        return {
            "city": city_name,
            "days": [
                {"hi": base + i, "lo": base + i - 5}
                for i in range(days)
            ],
        }


class LegacyMetAdapter(WeatherService):
    def __init__(self, legacy):
        self._legacy = legacy

    def current_temperature(self, city):
        return self._legacy.get_temp_c(city)

    def five_day_forecast(self, city):
        raw = self._legacy.get_forecast(city, 5)
        return [
            {"day": i + 1,
             "high_c": raw["days"][i]["hi"],
             "low_c": raw["days"][i]["lo"]}
            for i in range(5)
        ]

Your task

In main.py build:

  1. Abstract WeatherService(ABC) with:
    • current_temperature(self, city) → returns int/float
    • five_day_forecast(self, city) → returns a list of 5 dicts, each with keys day (1-based), high_c, low_c
  2. LegacyMet class (simulates the third-party library):
    • get_temp_c(city_name) returns a preset int per city. For unknown cities, returns 15.
    • get_forecast(city_name, days) returns a dict: {"city": <name>, "days": [{"hi": T+i, "lo": T+i-5} for i in range(days)]} where T = get_temp_c(city).
  3. LegacyMetAdapter(WeatherService):
    • __init__(self, legacy) stores the LegacyMet instance
    • current_temperature delegates to get_temp_c
    • five_day_forecast calls get_forecast(city, 5) and translates the shape into what the interface expects

Structural tests enforce:

  • LegacyMetAdapter is a WeatherService (isinstance check).
  • The adapter does NOT inherit from LegacyMet (composition, not inheritance).
  • Callers can swap in a fake WeatherService and everything works — the adapter is not required.

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
06-adapter-starter