What It Is
The Visitor pattern adds a new operation to a family of types without changing them. The types learn one thing once — accept(visitor) — and from then on, every new operation is just a new visitor with one method per type.
It flips the usual trade: adding operations becomes easy, adding types becomes the expensive change.
Example: Operations over Shapes
The shapes never change again. Printing areas is one visitor, naming them is another — and an export-to-SVG visitor tomorrow would touch neither Circle nor Rectangle.
New operations, untouched types
Both visitors walk the same shape list through the same accept door. Each visitor holds the whole operation in one place, instead of scattering it across the types.
Output: complete Python program
from dataclasses import dataclass
from typing import Protocol
# A visitor is one whole operation over every shape type.
class Visitor(Protocol):
def visit_circle(self, circle: "Circle") -> None: ...
def visit_rectangle(self, rectangle: "Rectangle") -> None: ...
@dataclass
class Circle:
radius: float
def accept(self, visitor: Visitor) -> None:
visitor.visit_circle(self)
@dataclass
class Rectangle:
width: float
height: float
def accept(self, visitor: Visitor) -> None:
visitor.visit_rectangle(self)
class AreaPrinter:
def visit_circle(self, circle: Circle) -> None:
print(f"circle area: {3.14159 * circle.radius * circle.radius:.2f}")
def visit_rectangle(self, rectangle: Rectangle) -> None:
print(f"rectangle area: {rectangle.width * rectangle.height:.2f}")
class NamePrinter:
def visit_circle(self, circle: Circle) -> None:
print("a circle")
def visit_rectangle(self, rectangle: Rectangle) -> None:
print("a rectangle")
shapes = [Circle(radius=2), Rectangle(width=3, height=4)]
for shape in shapes:
shape.accept(AreaPrinter())
for shape in shapes:
shape.accept(NamePrinter())
When to Use It
Use it when a stable family of types keeps growing new operations — compilers visiting syntax trees, documents gaining exporters, shapes gaining renderers and measurers.
Skip it when the type family still grows: every new type forces a change in every visitor. In many Python programs a match statement over the types does the same job with less machinery — reach for the visitor when the operations, not the types, are multiplying.
Pros and Cons
Pros: whole operations live in one class each; new operations need zero changes to the visited types; related logic stops being scattered.
Cons: the double dispatch (accept calling back) confuses newcomers; adding one visited type means editing every visitor; the visited family's interface is frozen early.