Every time you read a file as text in Python, you are using an adapter. The file on disk is bytes. Your code wants strings. Something has to sit between the two and translate, and that something is io.TextIOWrapper:
import io
raw = io.BytesIO("café\n".encode("utf-8"))
text = io.TextIOWrapper(raw, encoding="utf-8")
print(type(raw).__name__, "->", repr(text.readline()))
# BytesIO -> 'café\n'
The Python docs describe TextIOWrapper as a buffered text stream providing higher-level access to a binary stream. The binary stream is not changed. It does not learn about encodings. A thin object holds it, converts on the way through, and presents the interface the caller expected. That is the whole pattern.
My claim in this issue: Adapter is less a pattern you decide to use and more a boundary you are already drawing, well or badly. The useful question is not "should I use an adapter" but "where does the translation live, and is it allowed to grow".
The definition, without the UML
The Gang of Four describe Adapter as converting the interface of a class into another interface clients expect, so classes with incompatible interfaces can work together. Three roles matter. The client wants some interface. The adaptee is the existing thing with the wrong interface, and you usually cannot or should not change it. The adapter implements the client's interface and forwards to the adaptee.
Java has the cleanest example in its standard library. The InputStreamReader documentation calls it a bridge from byte streams to character streams: it reads bytes and decodes them into characters using a specified charset. Same shape as Python, same reason. Two worlds with different vocabularies, one small class between them.
Go does it with a function. The net/http package defines a Handler interface with a single ServeHTTP method, and HandlerFunc is a type whose only job is to let an ordinary function satisfy it. The package documents it as an adapter for exactly that purpose. I ran it to check:
func hello(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "hello") }
var h http.Handler = http.HandlerFunc(hello)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
fmt.Println(rec.Code, rec.Body.String())
// 200 hello
Notice what HandlerFunc is not doing. It does not add behavior, validate anything, or make decisions. It makes one shape fit another shape. Hold on to that, because everything that goes wrong with adapters is the adapter doing something else.
The case that pays your rent: wrapping a vendor SDK
Most production adapters are not in the standard library. They sit around third-party code you do not control. A payment provider is the standard example, because the vendor's idea of a charge and your domain's idea of a charge never match.
Say the vendor SDK takes a float amount, an uppercase currency code, and a customer reference. Your domain thinks in integer cents. Here is the adapter, plus the interface your code actually depends on:
from typing import Protocol
class Charge(Protocol):
def charge(self, cents: int, currency: str, customer: str) -> str: ...
class AcmePay: # vendor SDK, cannot change
def create_payment(self, amount: float, ccy: str, cust_ref: str) -> dict:
return {"id": "pay_1", "amount": amount, "status": "ok"}
class AcmeAdapter:
def __init__(self, sdk: AcmePay):
self._sdk = sdk
def charge(self, cents, currency, customer):
r = self._sdk.create_payment(cents / 100, currency.upper(), customer)
if r["status"] != "ok":
raise RuntimeError("payment failed")
return r["id"]
The checkout code takes a Charge and never imports the vendor. Swapping the vendor means writing a second adapter, not touching checkout. Testing means a fake that records calls:
class FakeGateway:
def __init__(self): self.calls = []
def charge(self, cents, currency, customer):
self.calls.append((cents, currency, customer)); return "fake_1"
def checkout(gw: Charge, cents):
return gw.charge(cents, "eur", "cus_9")
print(checkout(AcmeAdapter(AcmePay()), 1999)) # pay_1
fg = FakeGateway()
print(checkout(fg, 1999), fg.calls) # fake_1 [(1999, 'eur', 'cus_9')]
If this looks familiar, it is because it is the same move as the one I described in the issue on dependency injection: the client declares what it needs, and something else supplies it. Adapter is how you make a foreign object fit that declaration. It is also the "A" in Ports and Adapters, the hexagonal architecture from earlier in this series. A port is the interface your core owns. An adapter is the translation to the outside world on the other side of it. Same pattern, drawn at system scale.
Failure mode one: the leak
The adapter above has a bug that I wrote on purpose. It converts cents to a float with cents / 100. That is correct for the demo and wrong for money in general, and it is the typical way adapters fail: the adaptee's types leak through.
print(0.1 + 0.2) # 0.30000000000000004
print(1999 / 100) # 19.99
For 1999 cents the division happens to give a clean 19.99. The vendor accepted a float, so the adapter faithfully converted, and now the vendor's representation of money has crept into the code that touches your ledger. The adapter's job was to contain the vendor's quirks. A float amount is a vendor quirk, and containing it means the conversion happens exactly once, in one place, ideally with the vendor's own decimal or string type if it offers one, and a test that pins the rounding behavior.
The tell for a leaky adapter is an exception or a type from the adaptee escaping through its return value. If callers of charge ever catch AcmeError, the adapter has not adapted anything. It has renamed a method. Translate the errors too: the client's interface includes the failures it can raise, and those are part of the contract.
Failure mode two: the adapter that grows a brain
Adapters attract logic because they are the one place that knows both sides. Someone needs a retry, so it goes in the adapter. Someone needs idempotency keys, so those go in the adapter. Then a discount rule that depends on which vendor you are on. A year later the adapter is 600 lines, has its own database table, and nobody can say what it is.
Microsoft's write-up of the Anti-Corruption Layer pattern is blunt about this. It describes the layer as translation between two subsystems with different semantics, and its considerations list says to keep the layer focused on translation and not to put business rules or orchestration in it. It also lists latency, maintenance, scaling, and a single-point-of-failure risk as the costs of the layer, which is a fair reminder that a translation boundary is not free.
The anti-corruption layer is Adapter in its strategic form: not one class, but a whole boundary protecting a new model from a legacy one. The same rule applies at both scales. The moment the adapter decides something, it has become a domain service with a misleading name, and it is now the hardest thing in your codebase to replace, which defeats the purpose.
A practical test: could you write the adapter's unit test using only the two interfaces, with no business vocabulary? A test for AcmeAdapter should say "given 1999 cents and EUR, the SDK receives 19.99 and EUR". If you need to say "given a premium customer", logic has moved in that does not belong.
Failure mode three: the adapter over a moving target
The third problem is subtler. An adapter encodes your understanding of the adaptee at one moment. When the adaptee changes, the adapter is the only thing that notices, and only if you wrote a test that exercises the real behavior.
This is where fakes become dangerous. The FakeGateway above is a perfectly good stand-in for testing checkout, but it tests nothing about whether AcmeAdapter still matches the SDK. You need a second kind of test, usually called a contract or integration test, that runs the adapter against the real vendor sandbox or a recorded response, and fails when the vendor changes a field. Keep those few and slow, and keep the many fast tests on the fake. If you only have the fake, you have a beautiful seam and no evidence that what is behind it works.
Object adapter or class adapter
The GoF book describes two implementations. A class adapter inherits from the adaptee, which needs multiple inheritance and ties you to the adaptee's whole surface. An object adapter holds the adaptee as a field and forwards, which is what every example above does. In practice you use the object adapter, almost always. Composition lets you adapt a class you cannot subclass, adapt different adaptees behind one interface, and swap the adaptee at runtime in a test. The class adapter's main remaining use is in languages where the inheritance is cheap and the surface is tiny, and even there I would reach for composition first.
Function-shaped adapters, like Go's HandlerFunc, are a third option worth knowing. When the interface has one method, an adapter can be a type over a function, and the whole thing costs one line. A Python equivalent is a class with a callable field:
class HandlerFunc:
def __init__(self, f): self.f = f
def serve(self, req): return self.f(req)
print(HandlerFunc(lambda r: "hello " + r).serve("bob")) # hello bob
Adapter versus its look-alikes
Three patterns get confused with Adapter because they also wrap an object. The difference is intent, and intent is what you should name in the code review.
Decorator keeps the same interface and adds behavior. Adapter changes the interface and adds nothing. Facade invents a new, simpler interface over a subsystem of many objects, while an adapter targets an interface that already exists and is fixed by the client. Proxy keeps the same interface and controls access. If your wrapper has the same method names in and out, it is not an adapter. If it adds retries, it is not an adapter either, it is a decorator on top of one. The next issues in this series cover Decorator, Facade, and Proxy in turn, so the contrasts will get code.
When not to write one
An adapter is a seam, and seams have a cost: an extra type, an extra hop, one more place to look when something breaks. You do not need one when the adaptee's interface is already what the client wants, when the dependency is stable and unlikely to be swapped, or when the thing you are wrapping is a pure function you could call directly. Wrapping a well-known library such as a standard HTTP client behind your own interface "in case we switch" is how teams end up maintaining a worse copy of the library's API.
The rule I use: write the adapter when the foreign interface would otherwise leak into more than a couple of call sites, or when its types and errors do not match your domain. Before that point, call the library directly. After it, wrap it once and make everything else depend on your interface.
The short version
Adapter changes the shape of an interface and nothing else. It earns its place at the edges of your system, around vendors, legacy code, and the standard library's byte-versus-text style mismatches. It fails in three predictable ways: the adaptee's types leak through, business logic moves into it, and nobody tests it against the real thing. Keep it thin, translate errors as well as calls, and give the real adaptee one slow test and the fake many fast ones.
Next in the series: Decorator, which changes behavior without changing the interface.

