Python Visitor Pattern and Better Alternatives Explained

Five approaches break down the two-axis matrix design challenge of node types and operations, weighing the direction of change.
The article explores decoupling node types from operations in document editors as a two-axis matrix. It introduces five approaches: method pattern, pattern-matching functions, visitor pattern, dictionary mapping, and functools.singledispatch. Each suits different change scenarios. The conclusion: no silver bullet—choose based on which axis changes more often; single dispatch offers a good balance.
Title
Python Visitor Pattern and Better Alternatives Explained
Meta Description
A deep dive into Python's visitor pattern and four alternatives: method pattern, pattern matching, dictionary mapping, and functools.singledispatch. Starting from the two-axis matrix of nodes and operations, it helps you choose the most suitable decoupled design based on how frequently things change.
Article Content
When building systems like document editors, we often face a classic design challenge: how to decouple node types (paragraphs, images, sections) from operations (HTML rendering, accessibility checks, asset collection)? YouTube tech creator ArjanCodes breaks down this "matrix problem" with five approaches in one video and offers what he considers a more Pythonic solution.

The Essence of the Problem: A Two-Axis Matrix of Nodes and Operations
Suppose you're building a document editor with multiple node types: paragraphs, images, sections. Each node needs to support operations like HTML rendering, accessibility checks, and asset collection. The most intuitive approach is to write these operations as methods on the nodes—paragraphs have to_html(), images have to_html() and collect_assets(), and sections recursively delegate to child nodes. The code is direct and readable. When behavior naturally belongs to the model, or when new node types are added frequently while operations stay relatively fixed, this is the right choice.
But the cost is: every time you add an operation (say, Markdown export), all node classes must be modified. Nodes gradually become a "directory of unrelated operations," with bloated responsibilities.
Conversely, moving operations out of nodes and using standalone functions with match statements for dispatch aggregates rendering, checking, and collection each in one place—but now every operation must know all node types. The problem has merely shifted direction. ArjanCodes summarizes this dilemma in two dimensions: node types and operations form a matrix. The method pattern organizes by rows, external functions organize by columns, and the visitor pattern is the classic column-oriented solution.
The Visitor Pattern: The Classic Solution with an Explicit Contract
The visitor pattern creates one object per operation. In the video, a DocumentVisitor protocol class is defined, declaring methods like visit_paragraph, visit_image, and visit_section for each node type. Each node implements accept(visitor), calling the corresponding visit method internally.

Python's modern generic syntax is useful here: HTMLRenderer as DocumentVisitor[str] returns a string, while AccessibilityChecker returns list[str]. Compared to the pattern-matching version, the visitor pattern encapsulates operations as objects, making the contract between nodes and operations more explicit. Adding an operation only requires adding a visitor class—very lightweight. But adding a node type is costly—every visitor needs a new method. It suits systems where the model is relatively stable and operations keep evolving.
The visitor pattern centers on double dispatch: the first dispatch happens when a node calls accept(visitor), selecting the corresponding visit_xxx method based on node type; the second dispatch happens inside the visit_xxx method, executing the appropriate operation based on the visitor's concrete type. This structure means new operations only require new visitor classes without modifying node classes, adhering to the open-closed principle. The cost is that adding a new node type requires modifying all visitor interfaces and their implementations, so the visitor pattern is best suited for scenarios where "node types are stable and operations change frequently." In Python, visitor protocols are often defined via typing.Protocol, leveraging structural subtyping for implicit interfaces and avoiding the coupling of explicit inheritance.
Dictionary Mapping: Flattening the Matrix Explicitly
The third approach goes further: nodes are just simple data classes, operations are represented as functions, and dispatch happens via a mapping table from "node type × operation" to handler functions.

Specifically, you maintain several dictionaries keyed by node type with handler functions as values, e.g., HTML paragraph, HTML image, image accessibility check. Missing combinations can fall back to a default handler—if a paragraph has no accessibility issues and no assets, a default function returns an empty result. This "flattening the matrix into sequences" approach has the big advantage of gracefully defining default behavior: no registration, fallback. The downside is that multiple scattered dictionaries make the code a bit messy.
Single Dispatch: Maintaining the Matrix with Python Syntax
ArjanCodes' favorite is the functools.singledispatch approach. Define render_html(node) with a TypeError raised as a fallback, then use @render_html.register to register implementations for paragraphs, images, and sections. Accessibility checks work the same way, and a default implementation can be specified. At call time, you just write render_html(doc), and the single-dispatch mechanism automatically routes to the correct function based on the node's actual type.

This approach needs neither visitor objects nor hand-written dictionaries—dispatch is handled entirely by Python's internal mechanisms, while still supporting recursive calls on child nodes. For scenarios where "model and operations evolve independently," single dispatch maintains aggregation while avoiding the clutter of explicit mapping tables.
Single dispatch is a polymorphism mechanism in object-oriented programming that decides which function implementation to call based on the actual type of one argument. In contrast, multiple dispatch selects implementations based on combinations of multiple argument types. The functools.singledispatch decorator in Python's standard library brings this mechanism to the function level: the decorated function becomes the "default implementation," and then @func.register registers specialized implementations for specific types. At call time, the interpreter checks the type of the first argument and searches along its MRO (method resolution order) for the most matching registered function, falling back to the default implementation if none is found. Compared to class inheritance, single dispatch does not require node classes to implement specific interfaces or accept methods, thereby decoupling nodes and operations while maintaining type safety.
How to Choose: No Silver Bullet, Only Trade-offs
Each of the five approaches has its applicable boundary. The method pattern suits systems where behavior belongs to the model and node types vary; pattern-matching functions suit scenarios with static node types and concentrated operations; the visitor pattern suits architectures with a stable model but frequently added operations; dictionary mapping and single dispatch have advantages when default behavior and flexible extension are needed. The key is not to find the "best pattern" but to identify which axis in your system changes more frequently, and let the design grow in the direction of change.
The video author's judgment is pragmatic: in modern Python, single dispatch strikes a good balance between readability and flexibility. As for the final choice, it depends on whether your nodes or operations are more "fickle."
One-line Summary
Five approaches break down the two-axis matrix design challenge of node types and operations, weighing the direction of change.
Paragraph Summary
The article explores the classic design problem of decoupling node types from operations in document editors, abstracting it into a two-axis matrix of node types and operations. The author introduces five approaches: method pattern, pattern-matching functions, visitor pattern, dictionary mapping, and functools.singledispatch. The method pattern suits scenarios where behavior belongs to the model and nodes vary; pattern matching suits static nodes with concentrated operations; the visitor pattern suits architectures with a stable model but frequently added operations; dictionary mapping can gracefully define default behavior; single dispatch uses Python's internal mechanisms to maintain the matrix, balancing aggregation and flexibility. The conclusion is that there is no silver bullet—the choice depends on which axis changes more frequently in your system, and in modern Python, single dispatch achieves a good balance between readability and flexibility.
Related articles

OpenAI Partners with Shield: How the Astra Agent Revolutionizes IT Troubleshooting
OpenAI and Shield launch the Astra agent, which autonomously logs into computers and legacy systems to fix IT issues in real time, 35-40% faster than traditional models, with humans involved only when judgment is needed.

iPhone Duo Battery Life Tested: One Charge a Day Is Unavoidable, and the Large Screen Is the Culprit
iPhone Duo battery test: heavy use needs at least one charge a day, and about 30% remains after 6.5 hours of screen-on time. How often the large screen is unfolded is key; a MagSafe battery pack helps, and long-term degradation is also worth watching.

Kimi K3.1 Pro Open-Sourced: 2.8 Trillion Parameters Close In on Opus
Moonshot releases the open-source MoE model Kimi K3.1 Pro, with 2.8T total parameters and 104B activated, trailing Opus 5.5 by under 1 point on Terminal Bench 4.0.