Contents
Chapter 33

Visitor

A family of classes you cannot change needs a new operation. Visitor adds the operation from outside the family.

The Visitor pattern uses Multiple Dispatching. People can confuse the two by looking at the implementation rather than the intent.

Visitor assumes you have a primary class hierarchy you cannot change, perhaps because it comes from another vendor. You’d like to add new polymorphic methods to it, which normally means adding something to the base class interface. How do you add them from outside the base class?

The Classic Visitor

Visitor, the final pattern in GoF Design Patterns, solves the problem of a hierarchy you cannot change. It lets you extend the interface of the primary class hierarchy. It requires one method on the primary class hierarchy, typically called accept(), which takes an object from a secondary hierarchy called Visitor. That one method is the catch. The hierarchy’s author must write accept() in advance, because you cannot add it to a vendor’s classes yourself. accept() calls the visitor’s visit(), passing self. The visitor’s type chooses which visit() runs.

A new Visitor subclass that reuses an existing operation, such as Fly, adds no code to the primary hierarchy. In this Python version a new operation whose behavior varies by flower type also needs a new method on Flower, for a reason The Price of the Empty Base explains below. In the listing, bugs visit flowers:

# flower_visitors.py
import random
from collections.abc import Iterator
from typing import Any, override

# The Flower hierarchy cannot be changed:
class Flower:
    def accept(self, visitor: Any) -> None:
        visitor.visit(self)
    def pollinate(self, pollinator: Visitor) -> None:
        print(self, "pollinated by", pollinator)
    def eat(self, eater: Visitor) -> None:
        print(self, "eaten by", eater)
    def __str__(self) -> str:
        return type(self).__name__

class Gladiolus(Flower):
    pass
class Ranunculus(Flower):
    pass

class Chrysanthemum(Flower):
    @override
    def eat(self, eater: Visitor) -> None:
        print(self, "is toxic to", eater)

# The secondary hierarchy accepted by Flower:
class Visitor:
    def __str__(self) -> str:
        return type(self).__name__

class Bug(Visitor):
    pass

# The middle layer names the operation:
class Pollinator(Bug):
    def visit(self, flower: Flower) -> None:
        flower.pollinate(self)

class Predator(Bug):
    def visit(self, flower: Flower) -> None:
        flower.eat(self)

# Concrete visitors, grouped by the operation they perform:
class Bee(Pollinator):
    pass
class Fly(Pollinator):
    pass
class Worm(Predator):
    pass

def flower_gen(n: int) -> Iterator[Flower]:
    flowers = Flower.__subclasses__()
    for _ in range(n):
        yield random.choice(flowers)()

# Now perform Bug operations on the flowers:
if __name__ == "__main__":
    bee = Bee()
    fly = Fly()
    worm = Worm()
    random.seed(47)  # Reproducible flower sequence
    for flower in flower_gen(4):
        flower.accept(bee)
        flower.accept(fly)
        flower.accept(worm)
#: Ranunculus pollinated by Bee
#: Ranunculus pollinated by Fly
#: Ranunculus eaten by Worm
#: Gladiolus pollinated by Bee
#: Gladiolus pollinated by Fly
#: Gladiolus eaten by Worm
#: Ranunculus pollinated by Bee
#: Ranunculus pollinated by Fly
#: Ranunculus eaten by Worm
#: Chrysanthemum pollinated by Bee
#: Chrysanthemum pollinated by Fly
#: Chrysanthemum is toxic to Worm

flower_gen() reads the concrete classes from Flower.__subclasses__(), the same registry-free enumeration as Factory.

The accept()/visit() pair is the double dispatch. accept() passes the concrete flower to the visitor, visit() resolves the visitor’s type, and the pollinate() or eat() call inside visit() resolves the flower’s type. In the classic pattern every element class overrides accept(), and the override resolves the element’s type. In flower_visitors.py one inherited accept() is enough, because the pollinate() or eat() call resolves the flower’s type a step later.

The last line of output is the one where both dispatches change the result, because Chrysanthemum overrides eat() (chrysanthemums really do produce a natural insecticide). That line depends on both unknown types at once. The worm’s type chooses eat(), and the flower’s type chooses which eat() runs. If you delete the override, every flower resolves to the same Flower.eat(), and the visitor’s type alone decides which method runs.

flower_visitors.py’s output shows results, not mechanism. Printing the qualified name of the method to which each dispatch resolves makes both dispatches visible:

# dispatch_trace.py
from flower_visitors import Chrysanthemum, Gladiolus, Worm

worm = Worm()
for flower in (Chrysanthemum(), Gladiolus()):
    print(type(worm).visit.__qualname__,
          "then", type(flower).eat.__qualname__)
    flower.accept(worm)
#: Predator.visit then Chrysanthemum.eat
#: Chrysanthemum is toxic to Worm
#: Predator.visit then Flower.eat
#: Gladiolus eaten by Worm

The first dispatch resolves to Predator.visit rather than Worm.visit, because Worm inherits the operation from the class that defines it. For Chrysanthemum the second dispatch resolves to the override, and for Gladiolus to the inherited Flower.eat. The trace shows that flower-side choice, which flower_visitors.py’s output leaves implicit.

The Price of the Empty Base

One annotation in flower_visitors.py looks like a shortcut and is not. accept() types its visitor as Any because the Visitor base class declares no visit() method. If you declare that parameter as Visitor instead, the type checker reports visitor.visit as an unresolved attribute. The classic pattern declares visit() abstract on the visitor base, so visitor.visit resolves. A Protocol that declares visit() removes the Any:

class Visits(Protocol):
    def visit(self, flower: Flower) -> None: ...

class Flower:
    def accept(self, visitor: Visits) -> None:
        visitor.visit(self)

The chapter still keeps the empty Visitor base and the Any in accept(), because showing what the Any gives up is part of the point. What it gives up is every check on the visitor side. Gladiolus().accept(Bug()) passes the type checker and fails at runtime with AttributeError: 'Bug' object has no attribute 'visit'. In Multiple Dispatching, the Any in paper_scissors_rock.py lets a class that omits an eval_*() method pass the checker the same way. The Any in accept() is a choice, unlike the one in Data Transfer Objects, where a bag of attributes named at runtime leaves no precise type to write.

Notice which hierarchy holds the behavior. The classic pattern overloads visit() once per flower type and keeps each operation’s body in the visitor, so accept() is the one method it adds to the primary hierarchy. Python has no method overloading, since a second def visit() replaces the first. @overload does not change that. It declares extra signatures for the type checker, and the one implementation must still branch on the argument’s type. flower_visitors.py therefore puts the type-specific behavior in pollinate() and eat() on the flowers instead, and the visitors choose between them. So flower_visitors.py’s primary hierarchy holds the operations the pattern exists to keep out of it. A Python visitor can keep them instead by building the method name from the element’s class, as ast.NodeVisitor does with getattr(self, "visit_" + node.__class__.__name__), but the type checker cannot see which method a string names.

The Pythonic Visitor: singledispatch

Python can add an operation to an existing hierarchy from outside, using functools.singledispatch. singledispatch turns a plain function into one that dispatches on the type of its first argument, with per-type implementations registered from anywhere. Adding an operation from outside is what Visitor does, and singledispatch adds one without the accept() method or the Visitor class hierarchy. The flowers below are the same three as in flower_visitors.py. The two operations, nectar() and fragrance(), are new, each added independently of the other:

# visitor_singledispatch.py
from functools import singledispatch

class Flower:
    def __str__(self) -> str:
        return type(self).__name__

class Gladiolus(Flower):
    pass
class Ranunculus(Flower):
    pass
class Chrysanthemum(Flower):
    pass

# A new operation, defined outside the Flower hierarchy:
@singledispatch
def nectar(flower: Flower) -> str:
    return f"{flower}: no nectar"

@nectar.register
def _(flower: Gladiolus) -> str:
    return f"{flower}: abundant nectar"

@nectar.register
def _(flower: Chrysanthemum) -> str:
    return f"{flower}: a little nectar"

# A second operation, added independently of the first:
@singledispatch
def fragrance(flower: Flower) -> str:
    return "faint"

@fragrance.register
def _(flower: Ranunculus) -> str:
    return "strong"

if __name__ == "__main__":
    flowers: list[Flower] = [
        Gladiolus(), Ranunculus(), Chrysanthemum()]
    for f in flowers:
        print(nectar(f), "| fragrance:", fragrance(f))
    print(sorted(t.__name__ for t in nectar.registry))
    print(nectar.dispatch(Ranunculus)
          is nectar.dispatch(Flower))
#: Gladiolus: abundant nectar | fragrance: faint
#: Ranunculus: no nectar | fragrance: strong
#: Chrysanthemum: a little nectar | fragrance: faint
#: ['Chrysanthemum', 'Gladiolus', 'object']
#: True

@nectar.register reads the annotation on the implementation’s first parameter. flower: Gladiolus stores that implementation in the dispatch table under the key Gladiolus. A union annotation, flower: Gladiolus | Ranunculus, registers one implementation for several types at once.

Each registered implementation takes the name _. The name is free to be anything, since a call to nectar() finds that implementation through the dispatch table. _ is the conventional placeholder for a name that exists only to satisfy def. Reusing _ for every registration is safe. @nectar.register stores the function in its dispatch table before the next def _ rebinds the name, so every implementation stays in the table.

Nothing edits Flower. Each operation is a separate function.

Dispatch follows inheritance. A subclass resolves to its nearest registered ancestor. Every class descends from object, and @singledispatch registers the base implementation under object, so a type with no other registered ancestor gets the default.

visitor_singledispatch.py’s last two output lines inspect the dispatch table the decorator built. nectar.registry maps each registered type to its implementation, and nectar.dispatch(cls) reports the implementation to which cls resolves. Nothing registers Ranunculus, so Ranunculus resolves to the same implementation Flower does, the one stored under object.

The default is also the risk. A new Flower subclass gets the default answer until someone registers it. Both the runtime and the checker accept the nectar() call on that subclass, so a forgotten registration produces a wrong result rather than a failure.

The default also accepts arguments outside Flower. Because the default sits under object, not under the Flower in its annotation, nectar(42) returns 42: no nectar. ty and Pyright accept nectar(42) too, because the dispatcher that @singledispatch builds declares its parameters as Any. Only mypy rejects it, since mypy’s built-in singledispatch plugin checks the call against the base function’s signature.

When the default answer would be wrong for an unregistered type, give the base function a raise NotImplementedError(f"no nectar rule for {type(flower).__name__}") in place of the fallback string. A forgotten registration then fails at its first call. A match over a union of types, with assert_never() in its case _, goes further. The type checker reports the omission before anything runs. The set of types is then closed, since adding a type means editing the union.

Under @singledispatch, adding a new operation is a new function. Adding a new flower is a class, plus one registration for each operation that needs more than the default.

When the operation should read like a method, use functools.singledispatchmethod instead. singledispatchmethod dispatches on the first argument after self.

Testing the Operations

Because each operation is a plain function, a test calls it with each flower type and asserts the result. The cases worth covering are the registered types, the @singledispatch default for an unregistered type, and a subclass that resolves to its nearest registered ancestor:

# test_visitor.py
import pytest
from visitor_singledispatch import (Chrysanthemum, Flower,
                                    Gladiolus, Ranunculus,
                                    fragrance, nectar)

@pytest.mark.parametrize("flower, expected", [
    (Gladiolus(), "Gladiolus: abundant nectar"),
    (Chrysanthemum(), "Chrysanthemum: a little nectar"),
    (Ranunculus(), "Ranunculus: no nectar"),
    (Flower(), "Flower: no nectar"),
])
def test_nectar_registered_and_default(
    flower: Flower, expected: str
) -> None:
    assert nectar(flower) == expected

@pytest.mark.parametrize("flower, expected", [
    (Ranunculus(), "strong"),
    (Gladiolus(), "faint"),
    (Chrysanthemum(), "faint"),
    (Flower(), "faint"),
])
def test_fragrance_registered_and_default(
    flower: Flower, expected: str
) -> None:
    assert fragrance(flower) == expected

def test_dispatch_follows_inheritance() -> None:
    class Hybrid(Gladiolus):
        pass

    assert Hybrid not in nectar.registry
    assert nectar(Hybrid()) == "Hybrid: abundant nectar"

Where Visitor Still Fits

Visitor still has a place: when an object must loop over its own elements inside accept(), or when someone else’s framework calls that method. Both cases are rare in Python. The recycling_note.py example in Pattern Refactoring reaches the same conclusion.

A minimal example shows the first of those cases, a loop that runs inside accept(). Corsage.accept() calls accept() on each of its elements and recurses into any nested Corsage, so no code outside the object loops over the elements:

# recursive_accept.py
from typing import Any
from flower_visitors import Bee, Gladiolus, Ranunculus

# accept() drives the traversal itself:
class Corsage:
    def __init__(self, *elements: Any) -> None:
        self.elements = elements

    def accept(self, visitor: Any) -> None:
        for element in self.elements:
            element.accept(visitor)

if __name__ == "__main__":
    corsage = Corsage(
        Gladiolus(), Corsage(Ranunculus()))
    corsage.accept(Bee())
#: Gladiolus pollinated by Bee
#: Ranunculus pollinated by Bee

The recursion works because Corsage and Flower both define accept(). The loop in Corsage.accept() calls accept() on each element the same way, whether the element is a flower or another corsage. In flower_visitors.py the loop that calls accept() sits outside the hierarchy, in the main block, and takes one flower at a time from flower_gen(). In recursive_accept.py the loop runs inside accept(), the situation where the classic pattern still fits.

One Dispatch Is Enough

Visitor dispatches twice, and singledispatch dispatches once. The figure sets Visitor and singledispatch side by side, with the worm from dispatch_trace.py and the nectar() operation from visitor_singledispatch.py:

Visitor’s two dispatches and singledispatch’s one: flower.accept(worm) calls Predator.visit, which calls back into Chrysanthemum.eat or the inherited Flower.eat, while nectar(f) looks the flower’s type up in one table with no method on Flower

In the first two frames the call crosses the boundary of Flower twice, out to the visitor and back again, and the frames differ only in the method the return trip reaches. In the third frame no method on Flower takes part. One lookup in nectar()’s table, keyed by the flower’s type, finds the answer. That one dispatch resolves everything Visitor’s two dispatches do.

The second dispatch in the classic pattern exists not because two types are unknown, but because the operation must be a method on some class. The visitor’s type stands in for the operation, so the language must resolve that type at runtime along with the element’s type. Once an operation can be a function defined outside the hierarchy, the call site names the operation, so writing nectar() rather than fragrance() selects it before the program runs. The flower’s type is the one thing left for runtime to resolve, and one dispatch covers it.

Visitor adds operations to a hierarchy you cannot edit, and its double dispatch is the means. Multiple Dispatching is the end in itself. Two objects must interact, and both their types stay unknown until runtime, as in paper_scissors_rock.py. singledispatch resolves one argument’s type, and the Multiple Dispatching problem needs two. When two types must genuinely resolve together, use the table keyed by a tuple of types from Multiple Dispatching.

Exercises

This chapter’s solutions give a hint, usually the shape of the code, and a full answer for each exercise.

  1. Rewrite flower_visitors.py with singledispatch. Make pollinate() and eat() functions defined outside the Flower hierarchy, with Chrysanthemum’s toxicity a registered implementation of eat(). Which classes and which methods disappear?
  2. Add a Rose to visitor_singledispatch.py with abundant nectar and a strong fragrance, then add a third operation, thorns(), over all four flowers. Count the lines each change costs, and say which of the two changes @singledispatch makes cheaper.
  3. Rewrite flower_visitors.py with the Visits protocol in place of Any, so accept() declares what it needs. Then add a Beetle(Bug) with no visit() method and pass it to accept(). Which version reports the mistake, and when?