Back to Blog
Python

Python PyYAML Custom Tags: Load and Dump Custom Types

python pyyaml custom tags: Learn how to register custom YAML tags in PyYAML with constructors and representers, load custom Python types safely, and handle unknown tags.

PyYAMLYAMLPythonCustom TagsSerialization
Abstract illustration of a YAML document tree with a highlighted custom tag node connected to a Python class symbol, representing PyYAML custom tag handling.

When you load a YAML document with PyYAML, the library maps each node to a Python object using a tag. Standard tags such as !!str, !!int, and !!map are handled by built-in constructors. When a document contains a type that does not map to a built-in Python type, you need to teach PyYAML how to interpret it. This is where python pyyaml custom tags come in: you register a constructor on a loader, and PyYAML invokes it every time it encounters the tag.

What Custom Tags Solve in PyYAML

A YAML tag is an annotation attached to a node that tells the parser what kind of value the node represents. In a document like this:

origin: !point x: 0 y: 0

the !point tag marks the mapping as a Point instance rather than a plain dictionary. Without a registered constructor, PyYAML has no way to know that !point should produce a Point object, so it falls back to a generic representation or raises an error depending on the loader.

Custom tags are useful when you control both the YAML files and the Python code that consumes them. Configuration files, data exchange formats, and test fixtures are common places where a custom tag keeps the YAML readable while mapping directly to domain objects.

How PyYAML Resolves Tags During Loading

PyYAML's loader walks the document tree node by node. For each node, it looks up the node's tag in a registry that maps tag strings to constructor functions. The registry is a class-level dictionary on the loader class, populated by add_constructor.

A constructor is a function with this signature:

def constructor(loader, node): ...

It receives the loader instance and the node being processed, and returns a Python object. For a mapping node, you typically call loader.construct_mapping(node) to get a dictionary of the node's children, then build your object from that dictionary.

The default yaml.Loader already has constructors for the standard YAML tags. When you add a custom tag, you are extending that registry with your own entry. The lookup happens at load time, so the constructor must be registered before yaml.load is called.

Defining a Constructor for a Custom Tag

Consider a Point class that stores x and y coordinates:

class Point: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f"Point({self.x}, {self.y})"

The constructor for the !point tag needs to read the mapping node and build a Point:

def point_constructor(loader, node): values = loader.construct_mapping(node) return Point(values["x"], values["y"])

construct_mapping returns a plain dictionary with the node's keys and values already resolved. Missing keys will raise a KeyError when you access them, so you may want to validate the mapping before constructing the object if the input is not fully trusted.

Registering the Tag on a Custom Loader

You attach the constructor to a loader by subclassing yaml.SafeLoader and calling add_constructor:

import yaml class PointLoader(yaml.SafeLoader): pass PointLoader.add_constructor("!point", point_constructor)

Now loading a document that uses !point produces a Point instance:

yaml_text = """ origin: !point x: 0 y: 0 """ data = yaml.load(yaml_text, Loader=PointLoader) print(data["origin"]) # Point(0, 0)

Using yaml.SafeLoader as the base is important. The plain yaml.Loader can construct arbitrary Python objects, which is a security risk when the YAML comes from an untrusted source. SafeLoader restricts the built-in constructors to basic types, and your custom constructor is the only extension point.

Dumping Custom Objects Back to YAML

Loading is only half of the problem. If you need to serialize a Point back to YAML with the same !point tag, you register a representer on a dumper:

def point_representer(dumper, point): return dumper.represent_mapping("!point", {"x": point.x, "y": point.y}) class PointDumper(yaml.SafeDumper): pass PointDumper.add_representer(Point, point_representer)

The representer receives the dumper instance and the object, and returns a node. represent_mapping creates a mapping node with the given tag and child values.

print(yaml.dump({"origin": Point(0, 0)}, Dumper=PointDumper))

Output:

origin: !point x: 0 y: 0

The constructor and representer are symmetric: the representer writes the tag and the mapping, and the constructor reads them back. If the two sides drift apart, round-tripping will fail or produce incorrect objects.

Security Boundaries When Using Custom Tags

The most important constraint with custom tags is that a constructor is arbitrary Python code. When PyYAML encounters a tag, it calls the registered constructor with the node's content. If the YAML is untrusted, an attacker can craft a document that triggers a constructor with unexpected data, or that uses a tag you did not intend to expose.

yaml.load with the default Loader is documented as unsafe because it can construct arbitrary Python objects. Always use yaml.SafeLoader or a subclass of it for any input that is not fully trusted. Your custom constructor should validate the node's structure before building the object, and should not perform side effects based on the node content.

AspectConstructorRepresenter
DirectionYAML to PythonPython to YAML
Registrationadd_constructoradd_representer
ReceivesLoader, NodeDumper, object
ReturnsPython objectNode

Handling Unknown Tags and Multi-Constructors

If a document contains a tag that has no registered constructor, SafeLoader raises a ConstructorError. In some cases you want to handle a family of tags with a single function. add_multi_constructor lets you register a constructor for a tag prefix:

def multi_constructor(loader, tag_suffix, node): values = loader.construct_mapping(node) return {"tag": tag_suffix, **values} class MultiLoader(yaml.SafeLoader): pass MultiLoader.add_multi_constructor("!app/", multi_constructor)

Now any tag starting with !app/ is handled by the same function, and the suffix after the prefix is passed as tag_suffix. This is useful when a YAML schema uses a namespace of related tags that all map to similar Python objects.

The tradeoff is that multi-constructors make the mapping between tags and types implicit. For a small, fixed set of tags, explicit add_constructor calls are clearer and easier to maintain. Use multi-constructors when the tag space is open-ended or generated dynamically.

python pyyaml custom tags: Practical Usage and Code Examples | RYUSLOG DEV