pyyaml
PyYAML is the standard YAML parser and emitter for Python: yaml.safe_load turns YAML text into dicts, lists, and scalars, yaml.dump goes the other way. It is a dependency of half the Python ecosystem (Ansible, Docker Compose tooling, CI config readers, Kubernetes clients) and at roughly 300 million downloads a week it is one of the most installed packages on PyPI. It implements the YAML 1.1 spec, with optional C-accelerated parsing through LibYAML bindings.
For reading YAML that other tools produce, PyYAML is the boring correct answer and safe_load is the only load you should ever type. For editing YAML in place or for strict 1.2 semantics it is the wrong tool, and ruamel.yaml exists precisely because of that.
Use it if
- You need to read YAML config files, Kubernetes manifests, CI definitions, or anything else the infrastructure world writes in YAML
- You want the zero-surprise choice: every tutorial, linter, and tool assumes PyYAML semantics, and it is probably already in your dependency tree
- You parse large YAML files and can install the LibYAML C bindings (yaml.CSafeLoader) for a substantial speedup over the pure-Python parser
- You only need load and dump; for that job the API has been unchanged and reliable for years
- You need to edit YAML and write it back preserving comments, ordering quirks, and formatting: PyYAML throws all of that away on load, ruamel.yaml is the round-trip tool
- You need YAML 1.2 behavior: PyYAML implements YAML 1.1, so unquoted 'yes', 'no', 'on', and 'off' become booleans and other 1.1 scalar rules apply, which regularly corrupts things like country code 'NO'
- You are choosing a config format for a new project you control: TOML has stdlib support via tomllib since Python 3.11 and avoids YAML's footguns entirely
- You are tempted by yaml.load on untrusted input: full load can construct arbitrary Python objects, which the README itself warns about; if a code reviewer will not catch that, prefer a parser without the dangerous mode
- You need new spec features to arrive: the issue tracker has a 350+ open issue and PR backlog and YAML 1.2 support has been an open request for years
Setup reality
pip install pyyaml just works because binary wheels ship for CPython on Linux, macOS, and Windows. The catch is the C accelerator: the wheels include LibYAML bindings when available, but yaml.load defaults to the pure-Python loader anyway, so you must explicitly pass CSafeLoader (with a try/except ImportError fallback) to get the speed you installed. Building from source without wheels needs a compiler and libyaml-dev headers. Historical trivia that still bites: version 6.0 broke installs for months in 2023 because of a Cython 3 build interaction on old pinned versions.
Patterns
Parse YAML from a fileload-yaml-safely
import yaml
with open('config.yml') as f:
config = yaml.safe_load(f)Always safe_load. Plain yaml.load with a full loader can execute arbitrary object construction from untrusted input.
Write a dict out as YAMLdump-yaml
import yaml
text = yaml.safe_dump(
data,
sort_keys=False,
default_flow_style=False,
allow_unicode=True,
)sort_keys defaults to True and will alphabetize your keys; set it False to keep insertion order.
Read a multi-document streammulti-document
import yaml
with open('manifests.yml') as f:
for doc in yaml.safe_load_all(f):
if doc is not None:
handle(doc)Kubernetes-style files separated by --- need load_all; empty documents come back as None, so filter them.
Use the LibYAML C parser when availablefast-c-loader
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
data = yaml.load(stream, Loader=SafeLoader)safe_load never uses the C parser on its own; you must pass CSafeLoader explicitly, with a fallback for wheels built without LibYAML.
Serialize a custom classdump-custom-class
import yaml
def point_representer(dumper, p):
return dumper.represent_mapping('!point', {'x': p.x, 'y': p.y})
yaml.add_representer(Point, point_representer, Dumper=yaml.SafeDumper)
yaml.safe_dump(Point(1, 2))Register against SafeDumper if you dump with safe_dump; registering on the default Dumper alone silently does nothing for safe_dump.
Parse a custom tag safelyload-custom-tag
import yaml
def env_constructor(loader, node):
import os
return os.environ.get(loader.construct_scalar(node), '')
yaml.SafeLoader.add_constructor('!env', env_constructor)
data = yaml.safe_load('token: !env API_TOKEN')Adding constructors to SafeLoader keeps safe_load safe while supporting the one tag you actually need.
Avoid the yes/no/on/off boolean trapnorway-problem
import yaml
yaml.safe_load('country: NO') # {'country': False} (YAML 1.1)
yaml.safe_load("country: 'NO'") # {'country': 'NO'}
yaml.safe_load('debug: on') # {'debug': True}This is YAML 1.1 behavior, not a bug; quote any value that could look like a boolean, and quote version numbers like 3.10 too or they load as floats.
Load from and dump to stringsstring-to-yaml-string
import yaml
data = yaml.safe_load('a: 1\nb: [2, 3]')
text = yaml.safe_dump(data) # returns a str when no stream givendump writes to the stream argument if given and returns None in that case; only the no-stream form returns the string.
Dump multi-line strings as literal blocksblock-style-strings
import yaml
def str_presenter(dumper, s):
style = '|' if '\n' in s else None
return dumper.represent_scalar('tag:yaml.org,2002:str', s, style=style)
yaml.add_representer(str, str_presenter, Dumper=yaml.SafeDumper)Without this, multi-line strings dump as quoted scalars full of \n escapes instead of readable | blocks.
Know what happens with duplicate keysduplicate-keys
import yaml
yaml.safe_load('a: 1\na: 2') # {'a': 2}, no errorPyYAML silently keeps the last duplicate key; if duplicates should be errors, validate separately or use a stricter parser.
Reuse blocks with anchors and merge keysanchors-aliases
import yaml
doc = '''\ndefaults: &d\n retries: 3\n timeout: 10\nprod:\n <<: *d\n timeout: 30\n'''
yaml.safe_load(doc)['prod'] # {'retries': 3, 'timeout': 30}Merge keys (<<) work on load, but dump never re-creates anchors from shared structure in a readable way; round-tripping loses them.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| ruamel.yaml | PyPI | You need round-trip editing that preserves comments and formatting, or YAML 1.2 semantics |
| strictyaml | PyPI | You want a restricted, schema-validated YAML subset that removes implicit typing footguns entirely |