Sometimes the problem you’re solving is as simple as “I don’t have the interface that I want.” Two of the patterns in GoF Design Patterns solve this problem. Adapter takes one type and produces an interface to some other type. Façade creates an interface to a set of classes. That interface makes a library or bundle of resources more comfortable to use. Both wrap something that already exists, which puts them next to Proxy and Decorator, and a later section sorts the four apart. Adding an interface is the safe half of the job. The other half is telling callers that the interface they have been using is going away.
When you’ve got “this”, and you need “that”, Adapter
solves the problem. The adapter only needs to produce a “that”.
A common real case: a third-party library names its methods
g() and h(), you wrote your code
against an f()-calling interface, and you cannot
change either one, so an adapter sits between them instead. The
smallest version puts the adaptation in an object of its
own:
# adapter.py
# The object adapter.
from typing import override
class WhatIHave:
def g(self) -> None:
print("WhatIHave.g()")
def h(self) -> None:
print("WhatIHave.h()")
class WhatIWant:
def f(self) -> None: ...
class ProxyAdapter(WhatIWant):
def __init__(self, what_i_have: WhatIHave) -> None:
self.what_i_have = what_i_have
@override
def f(self) -> None:
# Implement behavior using
# methods in WhatIHave:
self.what_i_have.g()
self.what_i_have.h()
class WhatIUse:
def op(self, what_i_want: WhatIWant, /) -> None:
what_i_want.f()
if __name__ == "__main__":
adapt = ProxyAdapter(WhatIHave())
WhatIUse().op(adapt)
#: WhatIHave.g()
#: WhatIHave.h()WhatIUse calls f() and
WhatIHave has none, so ProxyAdapter
supplies one and builds it out of the methods the adaptee does
have. WhatIWant is a bare placeholder rather than
an ABC or a Protocol, because this listing is about
where the adaptation lives, not how you declare the
target interface. Surrogate compares
an ABC with a Protocol. The name
ProxyAdapter takes a liberty with the term “Proxy”: GoF
Design Patterns requires a Proxy to have the same interface
as the object it speaks for.
The adaptation can live in two other places: the call site, or the adaptee’s own class.
# adapter_variations.py
# Two more places to put the adaptation.
from typing import Any, override
from adapter import (ProxyAdapter, WhatIHave, WhatIUse,
WhatIWant)
# Approach 2: build adapter use into op():
class WhatIUse2(WhatIUse):
@override
# def op(self, what_i_have: WhatIHave) -> None:
def op(self, what_i_have: Any) -> None:
ProxyAdapter(what_i_have).f()
# Approach 3: build adapter into WhatIHave:
class WhatIHave2(WhatIHave, WhatIWant):
@override
def f(self) -> None:
self.g()
self.h()
WhatIUse2().op(WhatIHave()) # Approach 2: adapting op()
#: WhatIHave.g()
#: WhatIHave.h()
WhatIUse().op(WhatIHave2()) # Approach 3: adapter built in
#: WhatIHave.g()
#: WhatIHave.h()The output is deliberately monotonous. Counting the object
adapter above, three structures produce one behavior: every
route ends at the same two methods on a WhatIHave.
The approaches differ only in where the adaptation lives. When
the output cannot tell them apart, only packaging separates
them. (GoF varies the same forwarding two further ways: a
pluggable adapter takes the adapting operation as a
delegate the client supplies, and a two-way adapter
presents both interfaces at once.)
The three split into two families GoF Design
Patterns names. ProxyAdapter is an object
adapter: it holds the adaptee and can wrap any instance
handed to it at runtime. WhatIHave2 is a class
adapter: it inherits from the adaptee. That inheritance
fixes the adapted class at definition time, and every client of
the adapter sees the adaptee’s entire surface, g()
and h() included. Composition keeps the two
interfaces separate. Inheritance merges them.
The / in WhatIUse.op() makes its
parameter positional-only. WhatIUse2.op() renames
that parameter to what_i_have. A caller passing it
by keyword would break on the rename, so ty and
Pyright reject a renamed keyword-capable parameter in an
override. The rename passes under mypy, which does not compare
parameter names in an override. Positional-only, the name is
invisible to callers and the rename is legal. The rename is the
smaller half of that story. WhatIUse2.op() also
changes the parameter’s type. The base version accepts a
WhatIWant, and the override accepts a
WhatIHave. If you annotate both precisely, a type
checker rejects the override outright, which ty
reports as invalid-method-override, because
narrowing what a method accepts breaks substitutability.
Uncomment the commented-out signature above,
what_i_have: WhatIHave, and the checker
reports:
error[invalid-method-override]: Invalid override of
method `op`
info: parameter `what_i_want` has an incompatible type:
`WhatIWant` is not assignable to `WhatIHave`
info: This violates the Liskov Substitution Principle
That is why this one parameter stays Any while
the rest of the listing names real types. The Any
is there so the type checker accepts an override that cannot
substitute for its base. Approach 2 is a different operation
under an inherited name. Code holding a WhatIUse
cannot safely receive a WhatIUse2, and that is the
price of building the adapter into the operation. The next
section argues Python lets you skip most of this packaging
too.
The variations above are Java habits. At runtime
WhatIUse.op() only calls f(), so any
object with an f() works and no shared base class
takes part. A type checker still holds you to the annotation, so
name the requirement with a Protocol
listing f() instead of a base class to inherit, the
same substitution Surrogate makes for
a proxy’s implementation. The common adapter need is “forward
most calls unchanged, and add or change a few.”
__getattr__() forwards the rest, so the adapter is
tiny:
# getattr_adapter.py
from typing import Any
class WhatIHave:
def g(self) -> str: return "g"
def h(self) -> str: return "h"
class Adapter:
def __init__(self, adaptee: WhatIHave) -> None:
self._adaptee = adaptee
def f(self) -> str: # The new interface
return self._adaptee.g() + self._adaptee.h()
# Forwards the rest
def __getattr__(self, name: str) -> Any:
return getattr(self._adaptee, name)
if __name__ == "__main__":
a = Adapter(WhatIHave())
print(a.f()) # Adapted method
print(a.g()) # Forwarded to the adaptee unchanged
#: gh
#: g__getattr__() runs only for attributes Python
does not find normally, so f() uses the adapter’s
own version while everything else falls through to the adaptee.
This is the idiomatic Python adapter: a thin wrapper, not a
hierarchy. Rethinking
Objects has a real one: PairCoord adapts a
Pair to the Coord protocol. It is a
frozen dataclass with two properties, written because
distance() requires x and
y while a Pair supplies a
and b. The forwarding carries the limits Surrogate
lists for __getattr__(). Special
methods bypass it, so an adapter that must support
adapter[key] or len(adapter) defines
those dunders, as exercise 1 does with
__getitem__(). The
recursion trap applies here too. copy.copy()
and pickle build an instance without running
__init__(), so _adaptee does not exist
yet, and __getattr__() reading
self._adaptee calls itself until Python raises a
RecursionError. An adapter that must survive
copying or pickling guards that lookup, or defines
__reduce__(), the hook pickle and
copy consult before ordinary construction.
The tests verify both halves of the adapter’s behavior. The
new f() combines the adaptee’s methods, and calls
to methods it doesn’t override forward to the wrapped
object:
# test_adapter.py
from getattr_adapter import Adapter, WhatIHave
def test_new_interface_combines_methods() -> None:
assert Adapter(WhatIHave()).f() == "gh"
def test_getattr_forwards_existing_methods_unchanged(
) -> None:
a = Adapter(WhatIHave())
assert a.g() == "g"
assert a.h() == "h"
def test_forwarding_targets_the_wrapped_object() -> None:
have = WhatIHave()
a = Adapter(have)
# __getattr__ delegates to adaptee
assert a.g.__self__ is haveIf something is ugly, hide it inside an object.
That is Façade. If you have a confusing collection of classes and interactions the client programmer doesn’t need to see, create an interface that presents only what’s necessary.
A Façade is often a Singleton Abstract Factory. A class containing static factory methods gets that effect:
# facade.py
from dataclasses import dataclass
@dataclass(frozen=True)
class Engine:
def start(self) -> None:
print("Engine.start()")
@dataclass(frozen=True)
class FuelPump:
engine: Engine
def prime(self) -> None:
print("FuelPump.prime()")
self.engine.start()
@dataclass(frozen=True)
class Ignition:
pump: FuelPump
def turn_key(self) -> None:
print("Ignition.turn_key()")
self.pump.prime()
class Facade:
@staticmethod
def start_car() -> Ignition:
ignition = Ignition(FuelPump(Engine()))
ignition.turn_key()
return ignition
Facade.start_car()
#: Ignition.turn_key()
#: FuelPump.prime()
#: Engine.start()Turning the key primes the pump, and priming starts the
engine: Ignition needs FuelPump, and
FuelPump needs Engine, in that order,
or the call sequence is wrong. That is the “confusing collection
of classes and interactions,” small enough to read in one glance
here; in real code, wiring three or thirty classes together in
the right order is exactly the mess a caller should never have
to know. Facade.start_car() hides the wiring and
the order behind one call that also builds the object, the
“static factory method” GoF pairs with Façade.
The cleaner Python façade is a module. A module
already presents a curated set of names over whatever tangle of
classes lives behind it. As Singleton
notes, it loads once, and every importer shares the same module.
At module level, put the friendly functions and the few classes
to expose. If you keep the messy internals private (using a
leading underscore, by convention), the import is
the façade:
# checkout.py
from dataclasses import dataclass
@dataclass(frozen=True)
class _TaxRule:
rate: float
@dataclass(frozen=True)
class _Discount:
fraction: float
@dataclass(frozen=True)
class _PriceEngine:
tax: _TaxRule
cut: _Discount
def compute(self, amount: float) -> float:
net = amount * (1 - self.cut.fraction)
return net * (1 + self.tax.rate)
def total(amount: float) -> float:
engine = _PriceEngine(_TaxRule(0.08), _Discount(0.10))
return engine.compute(amount)# checkout_demo.py
import checkout
print(f"{checkout.total(100.0):.2f}")
#: 97.20The caller imports one name. Three classes and their required
assembly order stay behind the underscore, and the façade can
rearrange them without touching a caller. The underscore is a
convention, not a barrier. checkout._PriceEngine
still resolves for anyone who types it. Mechanically, the
underscore keeps the name out of
from checkout import *, and an __all__
list of the public names states the same boundary explicitly. A
façade is an agreement about which names to call, not a lock on
the rest. A Facade class full of static methods
only reproduces what a module gives you, with more ceremony. checkout.py is one file; a
façade that outgrows one file scales the same way, one level up.
A package’s __init__.py re-exports a curated set of
names from private submodules, the same underscore convention,
applied to modules instead of classes. That is the idiomatic
place for a façade that fronts a whole subsystem, several
modules deep, GoF’s usual case for the pattern.
Façade has a failure mode too. An advanced caller who needs a name the façade never exposed has two bad options: reach past the underscore anyway, or wait for the façade’s author to widen the façade. If you widen it enough times, the façade stops simplifying anything; it just relays every name the subsystem has.
Adapter and Façade complete a family of wrappers that share
one structure, a front object forwarding to something behind it,
often through the same few lines of __getattr__().
Intent separates them, the distinction Design Patterns
says remains when structures match. When you cannot decide what
to call your wrapper, ask what breaks if you remove it:
| Wrapper | Interface | What it adds | Remove it and you lose |
|---|---|---|---|
| Proxy | same, by GoF’s definition | access control | control over when and whether the call gets through |
| Decorator | same | behavior | the added behavior |
| Adapter | changed | nothing | the fit between caller and callee |
| Façade | many narrowed to a few | nothing | the simplicity |
Surrogate
takes the looser view of the first row: a surrogate speaking for
its implementation is a Proxy whether or not the interfaces
match. Under that reading the same-interface rule no longer
separates a Proxy from an Adapter, which is why the
ProxyAdapter above answers to both names. That
leaves the “What it adds” column to separate them: a Proxy
controls access to one implementation, an Adapter makes one type
fit a caller that expects another. Name a wrapper for why it is
there, not for its shape.
Every interface change has a second half. Once the better
interface exists, the old one is still there, and callers keep
using it until something tells them not to. Deleting it breaks
them. Leaving it unmarked means nobody notices.
warnings.deprecated() (Python 3.13 and later;
typing_extensions.deprecated before that) marks a
function, method, or class as on its way out, and both a type
checker and the runtime understand the mark:
# deprecating.py
import warnings
class Report:
def render(self) -> str:
return "report"
@warnings.deprecated(
"Report.to_string() is replaced by render()")
def to_string(self) -> str:
return self.render()
report = Report()
print(report.render())
#: report
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
print(report.to_string()) # type: ignore
#: report
print(caught[0].category.__name__)
#: DeprecationWarning
print(caught[0].message)
#: Report.to_string() is replaced by render()to_string() keeps working, which is the point:
existing callers get a warning, not a break. The mark works in
two halves. The static half is a ty diagnostic on
the deprecated call, and the caller sees it before running
anything; the # type: ignore silences it here,
since this listing calls the deprecated method on purpose. The
runtime half is a DeprecationWarning. Python hides
those by default outside __main__ and test runners,
which is the trap: the caller who most needs the warning is the
least likely to see it. Run with
-W default::DeprecationWarning to see them all, or
-W error::DeprecationWarning in continuous
integration to fail on one. A warning also goes to standard
error, where a #: marker cannot capture it, so the
listing records the warnings and prints the record.
warnings.deprecated() requires the message, and
it should say what to use instead. “Deprecated” tells a reader
that someone decided to retire this. “replaced by
render()” tells them what to do about it. The
decorator also applies to a class, where it warns on
construction and on subclassing.
The finer instrument is to deprecate a single
@overload, warning about one call signature while
the rest stay current, so a function that used to take a string
and now takes a Path can warn only the string
callers. That form is static only. Python discards the overload
declarations at runtime, so the DeprecationWarning
half never fires. ty, Pyright, and mypy all report
a deprecated overload. Pyright and mypy need their deprecation
rule switched on, as they do for the whole-function form.
An Adapter and a Façade both add an interface without disturbing what is already there, which is why they are safe moves. Retiring an interface is the unsafe move, and marking the old interface is how you make the risk visible on a schedule instead of discovering it when you delete something.
PairsAdapter that wraps a list of
(key, value) tuples, following the shape of getattr_adapter.py. Give
it a dictionary-style __getitem__() that finds a
value by key, and forward every other attribute to the wrapped
list with __getattr__(). Confirm
adapter["name"] finds a value while
adapter.append(...) still reaches the underlying
list.deprecating.py, deprecate
the whole Report class instead of the method, and
show that constructing a Report warns while calling
render() does not.facade.py as a module
façade. Put its classes behind leading-underscore names in one
module, expose functions that build them, and import only those
from a second file. Compare what a caller can see in each
version.read() over an object that
only has next_chunk(), and one refuses calls unless
you set a flag. Classify each as Proxy, Decorator, Adapter, or
Façade using the “remove it and you lose” test from the table,
and say what you would lose in each case.