Behavioral Pattern

Visitor

Separates an algorithm from the object structure it operates on.

Visitor Pattern: A Complete Guide with Examples in 11 Programming Languages

The Visitor Pattern is a behavioral design pattern that lets you add new operations to a class hierarchy without modifying the classes themselves. Instead of cluttering each class with every operation it might ever need, you define a separate visitor object that contains the operation's logic. Each class in the hierarchy accepts a visitor and calls the visitor's method that corresponds to its own type. The class hierarchy stays untouched; new operations arrive as new visitor classes.

In this comprehensive guide, we'll explore the Visitor Pattern with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common pitfalls, and when to use this powerful pattern.

Table of Contents

What is the Visitor Pattern?

The Visitor Pattern solves a problem that appears whenever you have a stable class hierarchy — a fixed set of types that rarely changes — but you need to perform many different operations across all those types. The naive approach is to add every operation as a method to every class. A document model with Paragraph, Heading, Image, and Table nodes might end up with render(), exportToPdf(), countWords(), validate(), spellCheck(), and translate() all as methods on every node class. The nodes become bloated with logic that has nothing to do with their core responsibility of representing document structure.

The Visitor Pattern separates the data structure from the operations performed on it. Each class in the hierarchy keeps only one new method: accept(visitor). This method calls back to the visitor with the exact type of the object — visitor.visitParagraph(this), visitor.visitHeading(this), etc. The visitor object then handles the type-specific logic. To add a new operation, you add a new visitor class — the node classes never change.

Think of it like a document being audited by different inspectors. The document's pages and paragraphs don't change when a grammar checker arrives. They don't change when a word counter arrives, or a spell checker, or a PDF exporter. Each inspector (visitor) walks through the document (element hierarchy) doing their specific job. The document just accepts each inspector and shows them around.

Why Use the Visitor Pattern?

The Visitor Pattern offers several compelling benefits:

  1. Open/Closed Principle for Operations: Add new operations (visitors) without modifying any existing element class — new behavior is a new visitor class, not an edit to each node
  2. Single Responsibility: Each visitor class is responsible for exactly one operation across the hierarchy — rendering logic, export logic, and validation logic each live in their own class
  3. Accumulate State Across Operations: A visitor can collect results as it traverses the hierarchy — a WordCountVisitor accumulates a running total; a ValidationVisitor accumulates all errors found
  4. Type-Specific Dispatch Without Downcasting: The accept/visit mechanism provides double dispatch — the exact runtime type of the element determines which visit*() method is called, with no instanceof checks or casts
  5. Cross-Cutting Operations: Operations that touch multiple unrelated classes can be unified in one visitor — all the logic for one feature lives together rather than scattered across the hierarchy
  6. Separates Concerns Cleanly: The element classes own the data and structure; visitor classes own the operations — neither knows about the other's implementation details

Visitor Pattern Comparison

Let's compare the Visitor Pattern with related patterns:

PatternPurposeWho Adds BehaviorElement Changes?Use Case
VisitorAdd operations to a stable hierarchyNew Visitor classNoCompilers, document processors, AST analysis
StrategySwap algorithms at runtimeClient injects strategyNoSorting, routing, payment algorithms
CommandEncapsulate a request as an objectNew Command classNoUndo/redo, job queues, transactions
CompositeTree structures of objectsModify compositeYes (add accept)UI trees, file systems, document nodes
IteratorSequential traversal of a collectionNew IteratorMinimalList, tree, graph traversal

Key Distinctions:

  • Visitor vs. Strategy: Both add behavior without modifying existing classes, but they serve different purposes. Strategy swaps how a single operation is performed — the algorithm varies but there's one operation. Visitor adds new operations to a class hierarchy — the element types are fixed but the number of operations grows. Use Strategy when the variation is the algorithm; use Visitor when the variation is the operation performed on a family of types.
  • Visitor vs. Iterator: Both traverse a structure. Iterator provides a standard way to step through elements sequentially — it doesn't care what it does with each element. Visitor carries the operation with it — it knows exactly what to do with each element type. In practice, a Visitor often uses an Iterator (or recursion) internally to walk the structure.
  • Visitor vs. Decorator: Decorator wraps individual objects to add behavior transparently — the client doesn't know the wrapper is there. Visitor is explicitly accepted by the element — the element calls the visitor back. Visitor is better for bulk operations across a hierarchy; Decorator is better for per-object transparent augmentation.
  • Visitor vs. Command: Both encapsulate operations as objects. Command encapsulates a single action (with undo). Visitor encapsulates an operation across a type hierarchy — it has a different method for each element type. A Visitor could contain multiple Commands, one per element type.

Visitor Pattern Explained

The Visitor Pattern involves four participants:

Core Components:

  1. Visitor Interface: Declares a visit*() method for each concrete element type in the hierarchy. Every operation that needs to be performed on the hierarchy is a separate visitor class implementing this interface. The interface is the "contract" that ties visitors to elements.

  2. Concrete Visitors: Each concrete visitor implements the visit*() methods for all element types, providing the logic for one specific operation. A WordCountVisitor implements visitParagraph(), visitHeading(), visitTable() — each counting words in the respective element type. A PdfExportVisitor implements the same methods but with PDF rendering logic.

  3. Element Interface: Declares the accept(visitor) method that every element in the hierarchy must implement. This is the only change to the class hierarchy — one method per class. The accept() method is always identical in structure: call the visitor's method that corresponds to this element's type.

  4. Concrete Elements: Each element implements accept(visitor) by calling the visitor's corresponding visit*() method, passing itself as the argument. A Paragraph implements accept(visitor) as visitor.visitParagraph(this). This is the double dispatch mechanism — the element's runtime type selects the right visit*() method.

Double Dispatch Explained:

Standard method calls in most object-oriented languages are single dispatch — the runtime type of the receiver selects the method. element.render() selects the render() implementation based on element's type.

The Visitor Pattern implements double dispatch — the method selected depends on the runtime types of two objects: the visitor and the element. Without language-level support for multiple dispatch, this is achieved manually via the accept/visit handshake:

  1. element.accept(visitor) — first dispatch: selects based on element's type
  2. Inside accept: visitor.visitX(this) — second dispatch: selects based on visitor's type (which concrete visitor method is called)

The result: the correct visit*() method for the specific combination of visitor type and element type is invoked automatically, without any instanceof checks or type casting.

Traversal Strategies:

The Visitor Pattern says nothing about how to traverse the hierarchy. Traversal can be:

  • Visitor-driven: The visitor decides the traversal order and calls accept() on child elements from within the visit*() methods
  • Element-driven: Each element's accept() method first calls the visitor on itself, then calls accept() on its children — recursive tree traversal built into the element
  • External traversal: A separate iterator or walker traverses the structure and calls accept() on each element — maximum flexibility, decoupled from both visitors and elements

Class Diagram

Here's the UML class diagram showing the Visitor Pattern structure:

Beginner-Friendly Example

Let's start with the most intuitive Visitor example: a document node hierarchy that supports multiple operations. The document has Paragraph, Heading, Image, and Table nodes. We'll implement a WordCountVisitor that counts all words and an HtmlExportVisitor that renders the document to HTML. The node classes never change when we add new operations.

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List


# ── Visitor Interface ─────────────────────────────────────────────
class DocumentVisitor(ABC):
    @abstractmethod
    def visit_paragraph(self, paragraph: "Paragraph") -> None: ...

    @abstractmethod
    def visit_heading(self, heading: "Heading") -> None: ...

    @abstractmethod
    def visit_image(self, image: "Image") -> None: ...

    @abstractmethod
    def visit_table(self, table: "Table") -> None: ...


# ── Element Interface ─────────────────────────────────────────────
class DocumentElement(ABC):
    @abstractmethod
    def accept(self, visitor: DocumentVisitor) -> None: ...


# ── Concrete Elements ─────────────────────────────────────────────
class Paragraph(DocumentElement):
    def __init__(self, text: str) -> None:
        self.text = text

    def accept(self, visitor: DocumentVisitor) -> None:
        visitor.visit_paragraph(self)   # Double dispatch: routes to visitParagraph


class Heading(DocumentElement):
    def __init__(self, text: str, level: int = 1) -> None:
        self.text  = text
        self.level = level

    def accept(self, visitor: DocumentVisitor) -> None:
        visitor.visit_heading(self)


class Image(DocumentElement):
    def __init__(self, src: str, alt_text: str = "") -> None:
        self.src      = src
        self.alt_text = alt_text

    def accept(self, visitor: DocumentVisitor) -> None:
        visitor.visit_image(self)


class Table(DocumentElement):
    def __init__(self, rows: List[List[str]]) -> None:
        self.rows = rows   # First row is treated as header

    def accept(self, visitor: DocumentVisitor) -> None:
        visitor.visit_table(self)


# ── Concrete Visitors ─────────────────────────────────────────────
class WordCountVisitor(DocumentVisitor):
    """Counts all words across every element in the document."""

    def __init__(self) -> None:
        self._count = 0

    @property
    def count(self) -> int:
        return self._count

    def _count_words(self, text: str) -> int:
        return len(text.split()) if text.strip() else 0

    def visit_paragraph(self, paragraph: Paragraph) -> None:
        words = self._count_words(paragraph.text)
        self._count += words
        print(f"  [WordCount] Paragraph: {words} words")

    def visit_heading(self, heading: Heading) -> None:
        words = self._count_words(heading.text)
        self._count += words
        print(f"  [WordCount] Heading h{heading.level}: {words} words")

    def visit_image(self, image: Image) -> None:
        words = self._count_words(image.alt_text)
        self._count += words
        print(f"  [WordCount] Image alt text: {words} words")

    def visit_table(self, table: Table) -> None:
        words = sum(self._count_words(cell) for row in table.rows for cell in row)
        self._count += words
        print(f"  [WordCount] Table: {words} words")


class HtmlExportVisitor(DocumentVisitor):
    """Renders all document elements as HTML."""

    def __init__(self) -> None:
        self._parts: List[str] = []

    def get_html(self) -> str:
        return "\n".join(self._parts)

    def visit_paragraph(self, paragraph: Paragraph) -> None:
        self._parts.append(f"<p>{paragraph.text}</p>")

    def visit_heading(self, heading: Heading) -> None:
        self._parts.append(f"<h{heading.level}>{heading.text}</h{heading.level}>")

    def visit_image(self, image: Image) -> None:
        self._parts.append(f'<img src="{image.src}" alt="{image.alt_text}">')

    def visit_table(self, table: Table) -> None:
        rows_html = []
        for i, row in enumerate(table.rows):
            tag = "th" if i == 0 else "td"
            cells = "".join(f"<{tag}>{cell}</{tag}>" for cell in row)
            rows_html.append(f"  <tr>{cells}</tr>")
        self._parts.append("<table>\n" + "\n".join(rows_html) + "\n</table>")


class PlainTextExportVisitor(DocumentVisitor):
    """Renders all document elements as plain text — another visitor, zero changes to elements."""

    def __init__(self) -> None:
        self._lines: List[str] = []

    def get_text(self) -> str:
        return "\n".join(self._lines)

    def visit_paragraph(self, paragraph: Paragraph) -> None:
        self._lines.append(paragraph.text)

    def visit_heading(self, heading: Heading) -> None:
        underline = ("=" if heading.level == 1 else "-") * len(heading.text)
        self._lines.append(f"\n{heading.text}\n{underline}")

    def visit_image(self, image: Image) -> None:
        self._lines.append(f"[Image: {image.alt_text or image.src}]")

    def visit_table(self, table: Table) -> None:
        for row in table.rows:
            self._lines.append(" | ".join(row))


# ── Client ────────────────────────────────────────────────────────
if __name__ == "__main__":
    # Build a document (the element hierarchy)
    document: List[DocumentElement] = [
        Heading("Annual Report 2025", level=1),
        Paragraph("This report summarizes our annual performance and strategic initiatives."),
        Heading("Financial Highlights", level=2),
        Paragraph("Revenue grew by twenty percent year over year reaching record highs."),
        Image("chart.png", alt_text="Revenue growth chart showing upward trend"),
        Table([
            ["Metric", "2024", "2025"],
            ["Revenue", "$10M", "$12M"],
            ["Profit",  "$2M",  "$3M"],
        ]),
    ]

    print("=== Visitor Pattern: Document Processing ===\n")

    # Apply word count visitor — zero changes to document elements
    print("--- Word Count ---")
    wc = WordCountVisitor()
    for element in document:
        element.accept(wc)
    print(f"  Total: {wc.count} words\n")

    # Apply HTML export visitor — zero changes to document elements
    print("--- HTML Export ---")
    html_visitor = HtmlExportVisitor()
    for element in document:
        element.accept(html_visitor)
    print(html_visitor.get_html())

    # Apply plain text export visitor — zero changes to document elements
    print("\n--- Plain Text Export ---")
    txt_visitor = PlainTextExportVisitor()
    for element in document:
        element.accept(txt_visitor)
    print(txt_visitor.get_text())

Production-Ready Example

Now let's build an Abstract Syntax Tree (AST) evaluator — the textbook production use case for the Visitor Pattern. A simple expression language has Number, BinaryOp, and Variable nodes. Multiple visitors operate on the same AST: an EvaluatorVisitor computes the numerical result, a PrettyPrinterVisitor formats the expression as a string, and a TypeCheckerVisitor validates that the expression is well-formed. The AST nodes never change as we add new visitors.

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Dict, Optional
import operator


# ── AST Visitor Interface ─────────────────────────────────────────
class AstVisitor(ABC):
    @abstractmethod
    def visit_number(self, node: "NumberNode") -> object: ...

    @abstractmethod
    def visit_variable(self, node: "VariableNode") -> object: ...

    @abstractmethod
    def visit_binary_op(self, node: "BinaryOpNode") -> object: ...

    @abstractmethod
    def visit_unary_op(self, node: "UnaryOpNode") -> object: ...


# ── AST Element Interface ─────────────────────────────────────────
class AstNode(ABC):
    @abstractmethod
    def accept(self, visitor: AstVisitor) -> object: ...


# ── Concrete AST Nodes ────────────────────────────────────────────
class NumberNode(AstNode):
    def __init__(self, value: float) -> None:
        self.value = value

    def accept(self, visitor: AstVisitor) -> object:
        return visitor.visit_number(self)


class VariableNode(AstNode):
    def __init__(self, name: str) -> None:
        self.name = name

    def accept(self, visitor: AstVisitor) -> object:
        return visitor.visit_variable(self)


class BinaryOpNode(AstNode):
    SUPPORTED_OPS = {"+", "-", "*", "/", "**"}

    def __init__(self, op: str, left: AstNode, right: AstNode) -> None:
        if op not in self.SUPPORTED_OPS:
            raise ValueError(f"Unsupported operator: {op}")
        self.op    = op
        self.left  = left
        self.right = right

    def accept(self, visitor: AstVisitor) -> object:
        return visitor.visit_binary_op(self)


class UnaryOpNode(AstNode):
    def __init__(self, op: str, operand: AstNode) -> None:
        self.op      = op       # "-" for negation
        self.operand = operand

    def accept(self, visitor: AstVisitor) -> object:
        return visitor.visit_unary_op(self)


# ── Concrete Visitors ─────────────────────────────────────────────
class EvaluatorVisitor(AstVisitor):
    """Evaluates the AST to a numerical result given a variable environment."""

    OPERATORS = {
        "+":  operator.add,
        "-":  operator.sub,
        "*":  operator.mul,
        "/":  operator.truediv,
        "**": operator.pow,
    }

    def __init__(self, env: Optional[Dict[str, float]] = None) -> None:
        self._env = env or {}

    def visit_number(self, node: NumberNode) -> float:
        return node.value

    def visit_variable(self, node: VariableNode) -> float:
        if node.name not in self._env:
            raise NameError(f"Undefined variable: '{node.name}'")
        return self._env[node.name]

    def visit_binary_op(self, node: BinaryOpNode) -> float:
        left  = node.left.accept(self)
        right = node.right.accept(self)
        if node.op == "/" and right == 0:
            raise ZeroDivisionError("Division by zero in expression")
        return self.OPERATORS[node.op](left, right)

    def visit_unary_op(self, node: UnaryOpNode) -> float:
        val = node.operand.accept(self)
        return -val if node.op == "-" else val


class PrettyPrinterVisitor(AstVisitor):
    """Formats the AST as a human-readable infix expression string."""

    def visit_number(self, node: NumberNode) -> str:
        return str(int(node.value) if node.value == int(node.value) else node.value)

    def visit_variable(self, node: VariableNode) -> str:
        return node.name

    def visit_binary_op(self, node: BinaryOpNode) -> str:
        left  = node.left.accept(self)
        right = node.right.accept(self)
        # Parenthesize if operand is a BinaryOp (simplistic precedence handling)
        if isinstance(node.left, BinaryOpNode):   left  = f"({left})"
        if isinstance(node.right, BinaryOpNode):  right = f"({right})"
        return f"{left} {node.op} {right}"

    def visit_unary_op(self, node: UnaryOpNode) -> str:
        operand = node.operand.accept(self)
        if isinstance(node.operand, BinaryOpNode):
            operand = f"({operand})"
        return f"{node.op}{operand}"


class TypeCheckerVisitor(AstVisitor):
    """Validates the AST: checks all variables are declared and no division by literal zero."""

    def __init__(self, declared_vars: set[str]) -> None:
        self._declared = declared_vars
        self.errors: list[str] = []

    def visit_number(self, node: NumberNode) -> None:
        pass  # Numbers are always valid

    def visit_variable(self, node: VariableNode) -> None:
        if node.name not in self._declared:
            self.errors.append(f"Undeclared variable: '{node.name}'")

    def visit_binary_op(self, node: BinaryOpNode) -> None:
        node.left.accept(self)
        node.right.accept(self)
        if node.op == "/" and isinstance(node.right, NumberNode) and node.right.value == 0:
            self.errors.append("Division by literal zero detected")

    def visit_unary_op(self, node: UnaryOpNode) -> None:
        node.operand.accept(self)

    @property
    def is_valid(self) -> bool:
        return len(self.errors) == 0


class LatexExportVisitor(AstVisitor):
    """Renders the AST as a LaTeX math expression."""

    def visit_number(self, node: NumberNode) -> str:
        return str(int(node.value) if node.value == int(node.value) else node.value)

    def visit_variable(self, node: VariableNode) -> str:
        return f"\\{node.name}" if len(node.name) > 1 else node.name

    def visit_binary_op(self, node: BinaryOpNode) -> str:
        left  = node.left.accept(self)
        right = node.right.accept(self)
        if node.op == "/":
            return f"\\frac{{{left}}}{{{right}}}"
        if node.op == "**":
            return f"{left}^{{{right}}}"
        if isinstance(node.left, BinaryOpNode):  left  = f"\\left({left}\\right)"
        if isinstance(node.right, BinaryOpNode): right = f"\\left({right}\\right)"
        return f"{left} {node.op} {right}"

    def visit_unary_op(self, node: UnaryOpNode) -> str:
        operand = node.operand.accept(self)
        if isinstance(node.operand, BinaryOpNode):
            operand = f"\\left({operand}\\right)"
        return f"{node.op}{operand}"


# ── Client ────────────────────────────────────────────────────────
if __name__ == "__main__":
    print("=== AST Visitor Pattern ===\n")

    # Build AST for: (x + 2) * (y - 1)
    ast1 = BinaryOpNode("*",
        BinaryOpNode("+", VariableNode("x"), NumberNode(2)),
        BinaryOpNode("-", VariableNode("y"), NumberNode(1)),
    )

    printer  = PrettyPrinterVisitor()
    checker  = TypeCheckerVisitor(declared_vars={"x", "y"})
    evaluator = EvaluatorVisitor(env={"x": 3.0, "y": 5.0})
    latex    = LatexExportVisitor()

    print(f"Expression : {ast1.accept(printer)}")
    ast1.accept(checker)
    print(f"Type check : {'✅ Valid' if checker.is_valid else '❌ ' + str(checker.errors)}")
    print(f"Evaluate   : {ast1.accept(evaluator)}   (x=3, y=5)")
    print(f"LaTeX      : {ast1.accept(latex)}")

    # Build AST for: x ** 2 + -y  (with an undeclared variable z mixed in for type check demo)
    print("\n--- Expression with undeclared variable ---")
    ast2 = BinaryOpNode("+",
        BinaryOpNode("**", VariableNode("x"), NumberNode(2)),
        UnaryOpNode("-", VariableNode("z")),   # z is not declared!
    )
    checker2 = TypeCheckerVisitor(declared_vars={"x", "y"})
    ast2.accept(checker2)
    print(f"Expression : {ast2.accept(printer)}")
    print(f"Type check : {'✅ Valid' if checker2.is_valid else '❌ Errors: ' + str(checker2.errors)}")
    print(f"LaTeX      : {ast2.accept(latex)}")

    # Demonstrate adding a visitor with zero element changes
    print("\n--- Division by zero detection ---")
    ast3 = BinaryOpNode("/", VariableNode("x"), NumberNode(0))
    checker3 = TypeCheckerVisitor(declared_vars={"x"})
    ast3.accept(checker3)
    print(f"Expression : {ast3.accept(printer)}")
    print(f"Type check : {'✅ Valid' if checker3.is_valid else '❌ Errors: ' + str(checker3.errors)}")

Real-World Use Cases

1. Compiler and Interpreter ASTs

Every compiler and interpreter uses the Visitor Pattern on its Abstract Syntax Tree. The same AST is visited by a type checker, a constant folder, an optimizer, a code generator, and a source formatter. Each pass is a separate visitor. The AST node classes (IfStatement, FunctionCall, BinaryExpression, Literal) never change as new compiler passes are added. LLVM, GCC, Roslyn (C#), and the Java compiler all use this architecture.

2. Document Processing and Export

Office suites, static site generators, and content management systems use the Visitor Pattern to process document models. The same DOM (Paragraph, Heading, Image, Table, List) can be visited by an HTML exporter, a PDF renderer, a Markdown exporter, a word counter, a spell checker, and a search indexer — all independent visitor classes operating on the same stable document model.

3. File System Operations

File system traversal tools apply multiple operations to the same tree of File and Directory nodes: calculating total size, searching for files matching a pattern, deleting temporary files, changing permissions, generating a manifest. Each operation is a visitor. The File and Directory classes never change; the operations arrive as new visitors.

4. UI Rendering Pipelines

GUI frameworks with component trees use the Visitor Pattern for rendering passes. A Button, Label, Panel, and ScrollView hierarchy can be visited by a layout calculator, a paint visitor, a hit-test visitor (which component was clicked), an accessibility tree exporter, and a serialization visitor for UI snapshots. Each rendering concern is an independent visitor.

5. Tax and Financial Calculation Engines

Financial systems with different instrument types (Stock, Bond, Option, ETF, Futures) calculate taxes differently, report differently, and hedge differently. Each financial operation — tax calculation, mark-to-market valuation, risk exposure, regulatory reporting — is a visitor that knows how to handle each instrument type. Adding a new reporting requirement means adding a new visitor, not modifying any instrument class.

6. XML/JSON Schema Validation

Schema validators for structured data formats visit the node hierarchy (ObjectNode, ArrayNode, StringNode, NumberNode, BooleanNode) with a validation visitor that checks each node against its schema rules. A separate TransformVisitor converts the node tree from one schema to another. A PathExtractorVisitor finds all nodes matching a JSONPath expression. The node hierarchy is stable; the operations are visitors.

7. Game World and Scene Graph Processing

3D game engines and scene graph managers use the Visitor Pattern on scene nodes (MeshNode, LightNode, CameraNode, TransformNode, ParticleSystemNode). A RenderVisitor builds the render queue, a PhysicsVisitor collects collidable objects, a CullingVisitor prunes nodes outside the camera frustum, a SerializationVisitor saves the scene to disk, and an AnimationVisitor updates transforms each frame. The scene graph never changes; passes are visitors.


Language-Specific Mistakes and Anti-Patterns

❌ Mistake #1: Using isinstance Checks Instead of the Visitor Pattern

# BAD: Type-checking sprawl — every new operation requires editing this function
def export_to_html(element) -> str:
    if isinstance(element, Paragraph):
        return f"<p>{element.text}</p>"
    elif isinstance(element, Heading):
        return f"<h{element.level}>{element.text}</h{element.level}>"
    elif isinstance(element, Image):
        return f'<img src="{element.src}">'
    elif isinstance(element, Table):
        return "<table>...</table>"
    else:
        raise TypeError(f"Unknown element: {type(element)}")

# Adding a new operation (Markdown export) means writing another function with the same pattern.
# Adding a new element (Video) means finding and editing EVERY such function.

# GOOD: Visitor Pattern — new operations are new classes; element dispatch is automatic
class HtmlExportVisitor(DocumentVisitor):
    def visit_paragraph(self, p): return f"<p>{p.text}</p>"
    def visit_heading(self, h):   return f"<h{h.level}>{h.text}</h{h.level}>"
    def visit_image(self, i):     return f'<img src="{i.src}">'
    def visit_table(self, t):     return "<table>...</table>"

❌ Mistake #2: Forgetting to Return Values from visit*() Methods

# BAD: visit*() methods have side effects but no return value — only works for printing
class HtmlExportVisitor(DocumentVisitor):
    def visit_paragraph(self, p: Paragraph) -> None:
        print(f"<p>{p.text}</p>")  # Side effect only — can't compose or test output

    def visit_heading(self, h: Heading) -> None:
        print(f"<h{h.level}>{h.text}</h{h.level}>")

# Can't do: html_string = element.accept(visitor)
# Can't test without capturing stdout
# Can't pipe output to another operation

# GOOD: accept() returns a value — visitor is composable and testable
class HtmlExportVisitor(DocumentVisitor):
    def __init__(self) -> None:
        self._parts: list[str] = []

    def get_html(self) -> str:
        return "\n".join(self._parts)

    def visit_paragraph(self, p: Paragraph) -> None:
        self._parts.append(f"<p>{p.text}</p>")  # Accumulate for later retrieval

❌ Mistake #3: Putting Traversal Logic Inside the Visitor Instead of the Element

# BAD: Visitor accesses element internals to drive traversal — tight coupling
class WordCountVisitor(DocumentVisitor):
    def visit_document(self, doc: Document) -> None:
        for element in doc.elements:   # Visitor drives traversal — knows Document internals
            if isinstance(element, Section):
                for sub_el in element.sub_elements:   # Reaches into nested structure
                    sub_el.accept(self)
            else:
                element.accept(self)

# GOOD: Elements drive their own traversal — visitor just visits
class Section(DocumentElement):
    def __init__(self, elements: list) -> None:
        self.elements = elements

    def accept(self, visitor: DocumentVisitor) -> None:
        visitor.visit_section(self)
        for element in self.elements:  # Element drives traversal of its children
            element.accept(visitor)

Frequently Asked Questions

Q1: What problem does the Visitor Pattern actually solve?

The Visitor Pattern solves the expression problem: given a set of types and a set of operations, you want to be able to add new types without changing existing operations, AND add new operations without changing existing types. Neither classes alone nor function tables alone handle both simultaneously.

Traditional object-oriented design handles adding new types well (just add a subclass) but adding new operations badly (you must add a method to every existing class). The Visitor Pattern flips this: it handles adding new operations well (just add a new visitor class) but adding new types badly (you must add a visit*() method to every existing visitor).

The Visitor Pattern is the right choice when your hierarchy is stable (types rarely change) but your operations are growing (you keep adding new things to do with those types).

Q2: What is double dispatch, and why does the Visitor Pattern need it?

Single dispatch is what most OOP languages support: element.render() selects the right render() method based on the runtime type of element. The receiver's type selects the method.

Double dispatch selects the right method based on the runtime types of two objects — in this case, the element AND the visitor. Without double dispatch support, calling visitor.visit(element) would only dispatch on the visitor's type — it couldn't distinguish between a Paragraph and a Heading argument unless the visitor used instanceof checks.

The Visitor Pattern manually implements double dispatch via the accept/visit handshake:

  1. element.accept(visitor) — dispatches on element's type → calls visitor.visitParagraph(this) (if element is a Paragraph)
  2. visitor.visitParagraph(paragraph) — dispatches on visitor's type → calls the right visitor's method

The result: the exact method called depends on both the runtime type of the element (via the accept() override) and the runtime type of the visitor (via normal polymorphic dispatch). No instanceof, no casting.

Q3: When should I use Visitor instead of adding a method to each class?

Use adding a method when:

  • The operation is fundamental to the class's identity (e.g., area() on a Shape)
  • The operation will be needed by almost every client of the class
  • You expect to add new classes more often than new operations
  • The hierarchy is shallow and the operation is simple

Use Visitor when:

  • The operation is external to the class's core concern (e.g., spellCheck() on a Paragraph — spell checking is not a document structure concern)
  • You expect to add many new operations over time without changing the class hierarchy
  • The operation needs to accumulate state across multiple elements
  • Multiple unrelated operations need to be applied to the same hierarchy
  • The class hierarchy is stable but the set of operations is not

A good heuristic: if the method belongs to the class's "vocabulary" (what the class is and does), add it to the class. If the method is a use of the class from an external perspective (what you do with it), make it a visitor.

Q4: What are the downsides of the Visitor Pattern?

The Visitor Pattern has significant trade-offs:

Adding new element types is hard: Every time you add a new element class (e.g., VideoNode to a document model), you must add a visitVideo() method to every existing visitor interface and every concrete visitor class. If you have 20 visitors, you edit 21 files. This is the flip side of making operations easy to add.

Breaks encapsulation: Visitors often need access to the element's internals to do their work. This can force elements to expose fields or methods as public that would otherwise be protected or package-private. The visitor and the element become closely coupled.

Circular dependencies: The visitor interface must reference all element types; each element type must reference the visitor interface. This creates a circular reference chain that can complicate module organization in languages with strict dependency rules.

Complexity overhead: For small hierarchies with few operations, the pattern is overkill — a simple if/else or switch handles it more directly with less boilerplate.

Q5: How do Rust enums with match relate to the Visitor Pattern?

In Rust, an enum with match provides a form of exhaustive pattern matching that handles the same dispatch problem the Visitor Pattern solves in other languages — and it's enforced by the compiler.

enum DocumentElement { Para(String), Heading(String, usize) }

fn to_html(el: &DocumentElement) -> String {
    match el {
        DocumentElement::Para(text)        => format!("<p>{}</p>", text),
        DocumentElement::Heading(text, n)  => format!("<h{0}>{1}</h{0}>", n, text),
        // Missing any variant = compile error — exhaustiveness enforced!
    }
}

This is essentially the Visitor Pattern without the accept/visit indirection — it's the compiler-native equivalent. The trade-off is the same as the Visitor Pattern: adding a new variant means updating every match expression, just as adding a new element type means updating every visitor.

For Rust, match on an enum is idiomatic for closed type sets. Box<dyn Trait> with the full Visitor Pattern (trait + accept()) is used for open type sets that need runtime extensibility.

Q6: Can a visitor return a value?

Yes — and this is often cleaner than accumulating state. Use a generic return type on the visitor interface:

interface AstVisitor<T> {
    visitNumber(n: NumberNode):     T;
    visitVariable(v: VariableNode): T;
    visitBinaryOp(n: BinaryOpNode): T;
}

class EvaluatorVisitor implements AstVisitor<number>  { /* returns numbers */ }
class PrinterVisitor implements AstVisitor<string>     { /* returns strings */ }
class CheckerVisitor implements AstVisitor<boolean>    { /* returns booleans */ }

With return values, accept() also returns the visitor's result:

interface AstNode { <T> T accept(AstVisitor<T> v); }
// ...
double result = ast.accept(new EvaluatorVisitor(env));
String pretty = ast.accept(new PrettyPrinterVisitor());

This style — common in Java, TypeScript, Kotlin, and Rust — makes visitors pure functions (given the same AST, produce the same output) which are easier to test and compose.

Q7: How does the Visitor Pattern work with composite structures (trees)?

The Visitor Pattern works naturally with the Composite Pattern (trees of objects). Each composite node's accept() method calls the visitor on itself AND recursively calls accept() on all its children:

class Section(DocumentElement):
    def __init__(self, elements: list) -> None:
        self.elements = elements

    def accept(self, visitor: DocumentVisitor) -> None:
        visitor.visit_section(self)       # Visit the section itself
        for element in self.elements:
            element.accept(visitor)       # Recursively visit all children

The visitor doesn't need to know anything about the tree structure — it just handles each node type as it arrives. The tree handles its own traversal.

For fine-grained traversal control, the visitor can decide whether to continue into children:

def visit_section(self, section: Section) -> None:
    if self._should_skip_section(section):
        return  # Don't recurse into this section's children
    # Otherwise, section.accept() continues to call accept() on children

Q8: How do I handle the Visitor Pattern when I can't modify the element classes?

When you cannot add accept(visitor) to existing classes (third-party classes, legacy code, value types), you have two options:

External visitor via isinstance/instanceof: Use type-checked dispatch in the visitor instead of double dispatch. This loses the compile-time exhaustiveness guarantee but works without modifying the element classes:

def visit(self, element: object) -> None:
    if isinstance(element, Paragraph):
        self.visit_paragraph(element)
    elif isinstance(element, Heading):
        self.visit_heading(element)
    else:
        raise TypeError(f"Unknown element type: {type(element)}")

Adapter/Wrapper: Wrap the unmodifiable classes in your own element classes that implement accept(). The wrappers delegate all other operations to the wrapped object.

Q9: Should I use Visitor or just a switch/match statement?

For small, closed, stable hierarchies (3-5 types, known at compile time, unlikely to change), a switch/match/when expression is often the simpler and more idiomatic choice. Languages like Kotlin, Rust, Swift, and C# with sealed classes and exhaustive pattern matching enforce completeness at the compiler level — a missing case is a compile error.

For larger, open, growing hierarchies where you expect many operations to be added over time, the Visitor Pattern pays off. The visitor interface centralizes the "contract" of all element types, and each operation class is self-contained and independently testable.

The practical threshold: if you have more than ~5 operations on more than ~5 element types, consider the Visitor Pattern. Below that threshold, switch expressions are likely cleaner.

Q10: How do I test code using the Visitor Pattern?

The Visitor Pattern dramatically improves testability in both directions:

Testing visitors independently: Create an instance of each element type directly and call accept() with a specific visitor. Each visitor is a pure unit — given the same elements, it should produce the same output. No need to mock or stub; the elements are just data objects.

def test_html_export_paragraph():
    visitor = HtmlExportVisitor()
    Paragraph("Hello world").accept(visitor)
    assert visitor.get_html() == "<p>Hello world</p>"

def test_word_count_table():
    visitor = WordCountVisitor()
    Table([["Metric", "Value"], ["Revenue", "Ten million"]]).accept(visitor)
    assert visitor.count == 4  # 4 words total in the table

Testing elements independently: An element's only job is to call the right visit*() method with itself. Test this with a mock visitor that records which method was called:

def test_paragraph_calls_visit_paragraph():
    mock = MagicMock(spec=DocumentVisitor)
    para = Paragraph("test")
    para.accept(mock)
    mock.visit_paragraph.assert_called_once_with(para)

Key Takeaways

🎯 Core Concept: The Visitor Pattern separates operations from the objects they operate on. A stable class hierarchy (Paragraph, Heading, Image, Table) keeps only one new method — accept(visitor) — which calls the visitor's corresponding method with itself (visitor.visitParagraph(this)). All operation logic lives in visitor classes, not in the element classes. Adding a new operation means adding a new visitor class — the element hierarchy never changes. This is the pattern that powers compilers, document processors, and every system where a fixed data model needs an ever-growing set of operations.

🔑 Key Benefits:

  • Open/Closed for operations: New operations are new visitor classes — no changes to element classes
  • Single Responsibility: Each visitor class owns exactly one operation across the entire hierarchy
  • Double dispatch without casting: The accept/visit handshake dispatches on both the element's type and the visitor's type — no instanceof, no casts
  • Accumulate state across traversal: Visitors gather results as they traverse — a word counter, error collector, or render buffer all fit naturally
  • Cross-cutting cohesion: All logic for one operation lives together in one class rather than scattered across every element class

🌐 Language-Specific Highlights:

  • Python: Use ABC and @abstractmethod for both the visitor interface and the element interface; avoid isinstance-based dispatch — that's the anti-pattern the Visitor Pattern replaces; use a visit_* naming convention; prefer returning values from visitors for composability
  • TypeScript: Use a generic return type AstVisitor<T> on the visitor interface — eliminates mutable accumulation for pure visitors; keep concrete element classes unexported; use interface (not abstract class) for the visitor contract
  • Java: Use generics on the visitor interface (AstVisitor<T>) for type-safe return values; use record for immutable element value objects; mark accept() with the visitor's exact concrete type parameter; avoid raw-type visitor interfaces
  • JavaScript: Never dispatch via this.constructor.name — minifiers break it; always use explicit method calls in accept(); use #private fields for visitor state accumulation; use classes (not plain objects) for visitors with state
  • C#: Use sealed record for immutable elements; for closed hierarchies, consider switch expressions with pattern matching as a simpler alternative to full Visitor; keep IDocumentVisitor parameter types strongly typed; avoid dynamic dispatch
  • PHP: Use PHP 8.0+ constructor property promotion for element constructors; type all visit*() parameters; use readonly properties on elements; final on concrete elements where appropriate
  • Go: No inheritance means interfaces for both visitors and elements; use pointer receivers (*VisitorType) for stateful visitors — value receivers silently discard state mutations; embed a base struct with no-op methods to avoid forcing all element types to implement every visitor method; store elements as interface slices
  • Rust: Use &mut self on visitor trait methods when the visitor accumulates state; implement accept() on enums (not just structs) to centralize dispatch; use Box<dyn Trait> for element collections; use match on enums for closed type sets — it's the idiomatic Rust equivalent
  • Dart: Use abstract class for both the visitor and element interfaces; Dart 3 sealed class for closed element hierarchies with exhaustive switch dispatch; type all visit*() parameters explicitly; avoid dynamic visitor parameter types
  • Swift: Use protocol for both visitor and element; mark visitor methods mutating when using struct-based visitors with state; use inout some DocumentVisitor for generic visitor passing; use any existentials for heterogeneous visitor collections
  • Kotlin: Use fun interface for single-method visitor variants; object for stateless singleton visitors (no allocation per use); data class for immutable element value objects; for closed sealed hierarchies with few operations, when expressions are idiomatic — use full Visitor for growing operation sets

📋 When to Use Visitor:

  • You have a stable class hierarchy (types rarely change) but a growing set of operations (new things to do with those types)
  • You need to add new operations without touching the existing class hierarchy
  • An operation traverses multiple unrelated classes and accumulates state
  • You want each operation's logic in one place rather than scattered across all element classes
  • The elements need to be processed by operations that are defined externally — compiler passes, exporters, validators, analyzers

⚠️ When NOT to Use Visitor:

  • The class hierarchy changes frequently — every new element type breaks all existing visitors
  • The hierarchy is small (3-5 types) and operations are few — a switch/match/when expression is cleaner
  • Encapsulation is critical — visitors often need access to element internals, increasing coupling
  • You're in a language with native multiple dispatch (Julia, CLOS) — the pattern is unnecessary
  • Operations are simple enough to add directly as methods — don't over-engineer

🛠️ Best Practices Across Languages:

  1. Implement accept() in every concrete element — never try to dispatch from a base class using type checks; each concrete class knows its own type and calls the right visit*() method
  2. Use typed/generic return values — visitor methods that return T rather than void make visitors pure functions that are easier to test and compose
  3. Keep visitor state private — the visitor's accumulated result (count, HTML string, error list) should only be accessible via a getter, not mutated from outside
  4. Let elements drive traversal of their childrenaccept() should call the visitor on this, then call accept() on child elements; visitors should not need to know the element's internal structure
  5. One visitor interface per concern — don't create a god visitor interface with methods for unrelated element hierarchies; separate interfaces for separate hierarchies
  6. Use object or module-level singletons for stateless visitors (Kotlin/Python/Go) — stateless visitors don't need to be instantiated per use
  7. Make elements immutable — elements are data; use records, data classes, or frozen objects to prevent visitors from accidentally modifying elements during traversal

💡 Common Pitfalls:

  • instanceof-based dispatch in the visitor: If a visitor checks if element instanceof Paragraph instead of using accept(), the double dispatch is broken — you've re-introduced the type-checking that the pattern was designed to eliminate
  • Dispatching from the base class via type checks: A base DocumentElement class that calls visitor.visitParagraph(this) only if this instanceof Paragraph is wrong — each concrete element must override accept() itself
  • Value receiver instead of pointer receiver in Go: A value receiver visitor silently discards all state mutations — the count is always zero, the HTML is always empty
  • Mixing traversal responsibility: Visitors should not traverse into elements' internals; elements' accept() methods should drive traversal of their own children
  • Stateful visitor reuse without reset: A visitor that accumulates state (word count, HTML parts) must be freshly instantiated (or reset) for each document — reusing the same instance across multiple documents produces incorrect cumulative results
  • Not handling all element types: A visitor that silently does nothing for some element types (instead of throwing or logging) hides bugs — missing visit*() implementations are always errors for non-trivial operations

🎁 Advanced Techniques:

  • Generic/typed return values: interface AstVisitor<T> with T visitNumber(NumberNode n) lets evaluators return Double, printers return String, and checkers return Boolean — all from the same visitor structure, no mutable accumulation needed
  • Visitor + Composite: Combine Visitor with the Composite Pattern so composite nodes recursively call accept() on their children — the visitor traverses the entire tree automatically, visiting every node exactly once
  • Visitor registry: A Map<Class<?>, Consumer<Object>> maps element types to visit functions — enables dynamic visitor registration without modifying the visitor interface, at the cost of type safety
  • Intercepting visitor: Wrap a visitor to add cross-cutting behavior (logging, timing, filtering) around every visit*() call — a LoggingVisitor wraps any DocumentVisitor and logs before and after each visit
  • Multi-pass visitors: Chain multiple visitors on the same traversal — PassOneVisitor collects symbol tables; PassTwoVisitor uses them for type checking. In compilers, multiple passes over the same AST are standard.
  • Visitor + Observer: Notify observers when specific element types are visited — a ProgressObserver receives events as a large document tree is processed, enabling progress bars and streaming output
  • Lazy/streaming visitors: For very large element hierarchies, a visitor can be a coroutine (Python generator, Kotlin sequence, Rust async stream) that yields results one at a time instead of accumulating everything in memory first

The Visitor Pattern is the right tool when you have a fixed data model and a growing need to do different things with it. It appears in every serious compiler, document processor, and analysis tool. Once you understand it, you'll see it in LLVM's instruction visitors, Java's bytecode manipulation libraries, Spring's bean definition visitors, and CSS selector engines. It is the canonical solution to the expression problem — the cleanest way to add operations to a type hierarchy without touching the types themselves.


This guide completes the behavioral design patterns series. You now have comprehensive coverage of:

Each guide covers all 11 programming languages with production-ready examples, language-specific best practices, and comprehensive anti-pattern analysis.