Skip to content
FollowtheDamnCode

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

Elements accept visitors; visitors hold the operations

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

Three operations, zero edits to the node classes
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

ChangeMethods on elements (plain polymorphism)Visitor
New operationEdit every element classAdd one visitor class
New element typeAdd one classEdit 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

ProsCons
New operations without touching elements (Open/Closed for operations)New element types touch every visitor
Each operation's logic in one classVisitors often need elements to expose data
Visitors can accumulate state as they walkDouble 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.