ComparisonDifficulty: Intermediate2 min

Visitor Classes vs functools.singledispatch and match in Python

Python can choose a function by the argument's type without accept() on every class.

Python can choose code by an object's type without an accept method on every class, in two ways from the standard library and the language. This card writes an area total and an SVG export both ways, then sets out what each gives up beside the visitor classes.

functools.singledispatch turns a function into one that picks an implementation by the type of its first argument. match, from Python 3.10, compares a value against patterns in turn, and a class pattern such as Circle(radius=r) matches an instance of that class and pulls out its field.

py
import math
from dataclasses import dataclass
from functools import singledispatch


@dataclass
class Circle:
    radius: float


@dataclass
class Square:
    side: float


@singledispatch
def area(shape: object) -> float:
    raise TypeError(f"no area for {type(shape).__name__}")


@area.register
def _(shape: Circle) -> float:
    return math.pi * shape.radius**2


@area.register
def _(shape: Square) -> float:
    return shape.side**2


def export(shape: object) -> str:
    match shape:
        case Circle(radius=r):
            return f"<circle r='{r}'/>"
        case Square(side=s):
            return f"<rect width='{s}'/>"
        case _:
            raise TypeError(f"no export for {type(shape).__name__}")


shapes = [Circle(1), Square(2), Circle(0.5)]
print(f"total area: {sum(area(s) for s in shapes):.2f}")
print("\n".join(export(s) for s in shapes))
OutputRan with python:3.12-slim (Python 3.12.13)
total area: 7.93
<circle r='1'/>
<rect width='2'/>
<circle r='0.5'/>
`area` dispatches by registration, and `export` by `match`; neither shape has `accept`.

The output is a total area of 7.93 and three SVG tags. The shapes are plain dataclasses with no knowledge of any operation.

Three ways of writing an operation per element class
ConcernVisitor classessingledispatchmatch
Code on the element classesAn accept method eachNoneNone
A missing element classCaught when the visitor is created, if the methods are abstractCaught at the call, by the fallbackCaught at the call, by case _
State kept across visitsFields on the visitorPassed as argumentsPassed as arguments
Classes you cannot editCannot add acceptWorksWorks
Other languagesThe same pattern in any object-oriented languagePython onlyPython 3.10 and later, and similar features elsewhere

Choose match for an operation small enough to read in one function, and singledispatch when each case is long, or when other modules must add cases for their own classes, which they can do with register. Choose visitor classes when an operation keeps state across a walk, such as counting calls over a whole program, or when the codebase is shared with a language that has neither feature. The fourth row matters most in practice: a library's classes cannot be given an accept method, and both alternatives work on them anyway.