YAML's flexibility extends beyond basic data types through custom tags, which enable direct mapping between YAML content and Python objects. Instead of parsing generic dictionaries and manually instantiating classes, custom tags let PyYAML automatically construct specialized objects like configuration classes, data models, or domain-specific types. This guide demonstrates how to implement bidirectional serialization for seamless YAML integration.
Understanding Custom Tags
Custom tags tell PyYAML how to interpret specific YAML nodes. A tag like !server signals that the following data should become a ServerConfig object rather than a plain dictionary.
Implementing a Complete Custom Tag
Register both a representer (Python → YAML) and constructor (YAML → Python) for full round-trip support:
import yaml
class ServerConfig:
"""Configuration class for server settings."""
def __init__(self, host, port, ssl=False):
self.host = host
self.port = port
self.ssl = ssl
def __repr__(self):
return f"ServerConfig(host={self.host}, port={self.port}, ssl={self.ssl})"
def server_representer(dumper, data):
"""Convert ServerConfig object to YAML representation."""
return dumper.represent_mapping('!server', {
'host': data.host,
'port': data.port,
'ssl': data.ssl
})
def server_constructor(loader, node):
"""Construct ServerConfig object from YAML node."""
values = loader.construct_mapping(node)
return ServerConfig(**values)
yaml.add_representer(ServerConfig, server_representer)
yaml.add_constructor('!server', server_constructor)
yaml_content = """
!server
host: api.example.com
port: 443
ssl: true
"""
config = yaml.load(yaml_content, Loader=yaml.FullLoader)
print(config) # ServerConfig(host=api.example.com, port=443, ssl=True)
print(f"Connecting to {config.host}:{config.port}")
output = yaml.dump(config)
print(output)Output:
ServerConfig(host=api.example.com, port=443, ssl=True)
Connecting to api.example.com:443
!server
host: api.example.com
port: 443
ssl: trueMultiple Custom Tags
Register multiple tags for different domain objects:
import yaml
class DatabaseConfig:
def __init__(self, engine, name, host="localhost"):
self.engine = engine
self.name = name
self.host = host
class CacheConfig:
def __init__(self, backend, ttl=3600):
self.backend = backend
self.ttl = ttl
def db_representer(dumper, data):
return dumper.represent_mapping('!database', {
'engine': data.engine, 'name': data.name, 'host': data.host
})
def db_constructor(loader, node):
return DatabaseConfig(**loader.construct_mapping(node))
def cache_representer(dumper, data):
return dumper.represent_mapping('!cache', {
'backend': data.backend, 'ttl': data.ttl
})
def cache_constructor(loader, node):
return CacheConfig(**loader.construct_mapping(node))
yaml.add_representer(DatabaseConfig, db_representer)
yaml.add_constructor('!database', db_constructor)
yaml.add_representer(CacheConfig, cache_representer)
yaml.add_constructor('!cache', cache_constructor)
config_yaml = """
database: !database
engine: postgresql
name: app_production
host: db.example.com
cache: !cache
backend: redis
ttl: 7200
"""
config = yaml.load(config_yaml, Loader=yaml.FullLoader)
print(f"Database: {config['database'].engine}")
print(f"Cache TTL: {config['cache'].ttl}")Output:
Database: postgresql
Cache TTL: 7200Implicit Resolvers for Pattern-Based Detection
Automatically detect types based on patterns without explicit tags:
import yaml
import re
from datetime import date
yaml.add_implicit_resolver(
'!isodate',
re.compile(r'^\d{4}-\d{2}-\d{2}$'),
first=list('0123456789')
)
def isodate_constructor(loader, node):
value = loader.construct_scalar(node)
year, month, day = map(int, value.split('-'))
return date(year, month, day)
yaml.add_constructor('!isodate', isodate_constructor)
data = yaml.load("""
event: Conference
start_date: 2024-06-15
end_date: 2024-06-17
""", Loader=yaml.FullLoader)
print(f"Type: {type(data['start_date'])}") # <class 'datetime.date'>
print(f"Event starts: {data['start_date']}")Output:
Type: <class 'datetime.date'>
Event starts: 2024-06-15Scalar Value Tags
For simple transformations, use scalar constructors:
import yaml
import os
def env_constructor(loader, node):
"""Replace !env tag with environment variable value."""
var_name = loader.construct_scalar(node)
return os.environ.get(var_name, f"<{var_name} not set>")
yaml.add_constructor('!env', env_constructor)
config_yaml = """
database:
password: !env DATABASE_PASSWORD
host: !env DATABASE_HOST
"""
os.environ['DATABASE_PASSWORD'] = 'secret123'
os.environ['DATABASE_HOST'] = 'localhost'
config = yaml.load(config_yaml, Loader=yaml.FullLoader)
print(config['database']['password']) # secret123Output:
secret123Safe Custom Loaders
For production use, create a custom safe loader:
import yaml
class SafeConfigLoader(yaml.SafeLoader):
"""Custom loader with application-specific tags."""
pass
def server_constructor(loader, node):
values = loader.construct_mapping(node)
return ServerConfig(**values)
SafeConfigLoader.add_constructor('!server', server_constructor)
config = yaml.load(yaml_content, Loader=SafeConfigLoader)Method Reference
| Method | Purpose | Direction |
|---|---|---|
add_representer() |
Convert Python object → YAML | Dump |
add_constructor() |
Convert YAML → Python object | Load |
add_implicit_resolver() |
Auto-detect tags via regex | Load |
add_multi_representer() |
Handle class hierarchies | Dump |
By implementing custom tags, you create expressive, type-safe configuration systems that bridge YAML's human-readable format with Python's object-oriented design patterns.