Visitor
Add new operations to a fixed set of classes without editing them, by moving each operation into a visitor object.
Big idea
Visitor moves an operation over a structure of objects into its own class. Each element has one tiny method — accept(visitor) — that calls back the visitor method for its type (visit_heading, visit_image). Adding a new operation is one new visitor, with no edits to the element classes.
The problem it solves
A document is a tree of Heading, Paragraph, Image, and CodeBlock nodes. Product keeps asking for operations: word count, HTML export, Markdown export, accessibility check (images need alt text), table of contents. Putting each into the node classes means every node grows to_html, to_markdown, word_count, check_a11y, toc_entry… The node classes become a dumping ground for unrelated features, and every new feature edits all four.
Think of it like
A building inspector visiting a construction site. The electrician, plumber, and fire inspector each walk through the same rooms; each room just lets the visitor in. The rooms don't need a "fire check" method — the fire inspector carries the expertise — and a new kind of inspection is a new inspector, not a renovation.
Structure
Double dispatch
node.accept(visitor) chooses based on the node's type; inside, visitor.visit_image(self) chooses based on the visitor's type. Two dynamic choices in a row — double dispatch — select the right code for the (element, operation) pair without any isinstance checks.
In code
class Heading:
def __init__(self, text): self.text = text
def accept(self, v): return v.visit_heading(self)
class Paragraph:
def __init__(self, text): self.text = text
def accept(self, v): return v.visit_paragraph(self)
class Image:
def __init__(self, src, alt=""): self.src, self.alt = src, alt
def accept(self, v): return v.visit_image(self)
class WordCount:
def visit_heading(self, h): return len(h.text.split())
def visit_paragraph(self, p): return len(p.text.split())
def visit_image(self, i): return 0
class HtmlExport:
def visit_heading(self, h): return f"<h2>{h.text}</h2>"
def visit_paragraph(self, p): return f"<p>{p.text}</p>"
def visit_image(self, i): return f'<img src="{i.src}" alt="{i.alt}">'
class AltTextCheck: # added later: node classes untouched
def visit_heading(self, h): return None
def visit_paragraph(self, p): return None
def visit_image(self, i): return None if i.alt else f"missing alt text: {i.src}"
doc = [Heading("Release notes"), Paragraph("Faster search and a new dark theme."),
Image("search.png", "Search results"), Image("theme.png")]
print("words:", sum(n.accept(WordCount()) for n in doc))
print("\n".join(n.accept(HtmlExport()) for n in doc))
print([p for p in (n.accept(AltTextCheck()) for n in doc) if p])output
words: 9
<h2>Release notes</h2>
<p>Faster search and a new dark theme.</p>
<img src="search.png" alt="Search results">
<img src="theme.png" alt="">
['missing alt text: theme.png']Run and edit it in the app.
With Visitor in place, which change is expensive?
Watch it
The trade-off, in one table
| Change | Methods on elements (plain polymorphism) | Visitor |
|---|---|---|
| New operation | Edit every element class | Add one visitor class |
| New element type | Add one class | Edit every visitor |
This is the classic expression problem. Pick the side that matches how your code actually changes.
When to use it — and when not to
Use it when the element classes are stable (an AST, a document model, a fixed set of shapes or file-system nodes) and operations keep being added: exporters, analysers, validators, pretty-printers, compilers' passes. Avoid it when new element types arrive often, or when there are only one or two operations — plain methods are simpler. In Python, functools.singledispatch or match on types can give a lighter-weight version.
Pros and cons
| Pros | Cons |
|---|---|
| New operations without touching elements (Open/Closed for operations) | New element types touch every visitor |
| Each operation's logic in one class | Visitors often need elements to expose data |
| Visitors can accumulate state as they walk | Double dispatch is unfamiliar to many readers |
How it relates
- It usually walks a Composite — a document tree, an Interpreter AST.
- Iterator delivers elements; Visitor operates on them.
- It's the opposite trade-off from ordinary Polymorphism.
- Command wraps one request; a visitor is a whole operation spread across many element types.
What does double dispatch achieve in Visitor?
Key terms
- Visitor — An object holding one operation's logic for every element type in a structure.
- accept() — The element method that calls back the matching visit method on a visitor.
- Double dispatch — Choosing code based on the runtime types of two objects.
- Expression problem — The trade-off between easily adding new types and easily adding new operations.