pyyaml review
PyYAML 6.0.3 parses YAML streams into Python scalars, lists, dictionaries, and tagged objects, and writes Python values back to YAML. SafeLoader limits construction to basic YAML tags, while FullLoader and UnsafeLoader accept wider tag sets and belong only on input you trust. The distribution has pure Python loader classes plus LibYAML-backed C variants. Version 6.0.3 adds Python 3.14 support and marks free-threading support experimental. Our Python 3.12 installation included a compiled extension, and import _yaml passed. The package does not ship py.typed, so strict type checking needs external stubs or local wrappers.
PyYAML 6.0.3 installed in 0.2 seconds as one 3 MB package with 0 dependencies and 0 pip-audit findings, and its compiled _yaml extension imported in 0.25 seconds on our box. Use it to consume YAML with safe loaders; choose ruamel.yaml for source-preserving edits, add duplicate-key validation for strict configuration, and avoid PyYAML when YAML 1.2 scalar behavior is mandatory.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 3 MB |
| Import | ✓ | import _yaml in 0.25s · compiled extensions · requires Python >=3.8 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does pyyaml install cleanly?
Yes. In a fresh container with an empty cache, pip install pyyaml finished in 0.2s, leaving 1 package and 3 MB on disk. pip-audit reported no known vulnerabilities.
What does pyyaml need to run?
Python >=3.8, and a platform wheel with compiled extensions. In our run import _yaml succeeded in 0.25s.
pyyaml or ruamel.yaml: which should you use?
ruamel.yaml: Use it when an editor must retain comments, quoting, and other source details across a YAML round trip. PyYAML 6.0.3 installed in 0.2 seconds as one 3 MB package with 0 dependencies and 0 pip-audit findings, and its compiled _yaml extension imported in 0.25 seconds on our box.
When should you not use pyyaml?
The application edits YAML files that humans maintain and must retain comments, quote choices, anchors, and layout. PyYAML rebuilds output from Python values; ruamel.yaml is built for round trips.
Discussed on
- hnBugs in the YAML specification102 points
- hnTips that may save you from the hell of PyYAML5 points
- hnCython and pyyaml is breaking many builds3 points
Use it if
- A Python service must consume YAML emitted by CI, deployment, infrastructure, or user configuration tools.
- You need ordinary parsing and emitting, and preserving comments, quotes, or the original layout is unnecessary.
- A deployed wheel includes LibYAML and the application can select CSafeLoader or CSafeDumper for its hot path.
- Custom tags, constructors, representers, tokens, or parse events are needed under application control.
- The application edits YAML files that humans maintain and must retain comments, quote choices, anchors, and layout. PyYAML rebuilds output from Python values; ruamel.yaml is built for round trips.
- YAML 1.2 scalar rules are required. Open issue #486 tracks support for the 1.2 core schema; current plain values such as yes, no, on, and off resolve under YAML 1.1 rules.
- Duplicate mapping keys must fail validation. Open issue #165 remains unresolved, and the ordinary constructor assigns each key into a dict, so the later value replaces the earlier one.
- Developers may call FullLoader or UnsafeLoader on untrusted input. The README explicitly directs untrusted streams to safe_load(), while UnsafeLoader resolves tags known to be unsafe.
- The project only needs a configuration format controlled at both ends. TOML through tomllib avoids YAML's implicit scalar resolution and custom tag surface.
Setup reality
We installed PyYAML 6.0.3 without a cache in an unprivileged Python 3.12 Bookworm container. The install finished in 0.2 seconds and left one package using 3 MB. It declared 0 direct dependencies and requires Python 3.8 or newer. The installed files contained compiled .so extensions, the license was MIT, and no py.typed marker was present. pip-audit found 0 known vulnerabilities. import _yaml succeeded in 0.25 seconds.
No credentials or configuration files are involved. Loader choice is the setup decision that matters. yaml.safe_load() uses the pure Python SafeLoader and constructs only basic tags. When the compiled module is available, select yaml.CSafeLoader explicitly; safe_load() does not switch to it automatically. Keep yaml.SafeLoader as a fallback if a target may install from source without LibYAML bindings. PyYAML 6.0.3 exposes the same split for SafeDumper and CSafeDumper.
Parsing does not preserve a document for editing. Comments and quote style are gone after construction, and safe_dump() writes a new representation. It sorts mapping keys by default unless sort_keys=False is passed. Plain yes, no, on, and off values resolve as booleans under the current YAML 1.1 resolver. Ordinary loaders also accept duplicate keys and keep the later value. Add schema validation and a duplicate-key loader when configuration mistakes must stop startup.
Use safe_load_all() for a stream containing more than one document. Each yielded item is still constructed in memory, so enforce upload-size limits before parsing untrusted files. Anchors can produce shared references and recursive data, which downstream code may not expect. Version 6.0.3 calls free-threading support experimental on Python 3.14. The package has no bundled typing marker, so verify any third-party stubs against the loaders and dump options your code actually uses.
Patterns
Read one untrusted YAML document load-safe-document
from pathlib import Path
import yaml
text = Path('config.yaml').read_text(encoding='utf-8')
config = yaml.safe_load(text)
if config is None:
config = {}safe_load() restricts construction to basic YAML tags. An empty document returns None rather than an empty dictionary.
Use LibYAML with a safe fallback load-with-c-extension
import yaml
try:
SafeLoader = yaml.CSafeLoader
except AttributeError:
SafeLoader = yaml.SafeLoader
config = yaml.load(text, Loader=SafeLoader)safe_load() always selects SafeLoader. Choose CSafeLoader explicitly when _yaml is installed, and keep a fallback for source-only targets.
Write Unicode YAML without sorting keys dump-readable-yaml
import yaml
text = yaml.safe_dump(
config,
allow_unicode=True,
sort_keys=False,
default_flow_style=False,
)safe_dump() sorts mapping keys by default. sort_keys=False retains Python insertion order, but comments and original quotes are not restored.
Iterate over a multi-document stream load-multiple-documents
import yaml
with open('resources.yaml', encoding='utf-8') as stream:
for document in yaml.safe_load_all(stream):
if document is not None:
deploy(document)safe_load() expects one document. safe_load_all() advances through documents separated by --- and yields each constructed value.
Emit an explicit multi-document stream dump-multiple-documents
import yaml
yaml.safe_dump_all(
documents,
stream=output,
explicit_start=True,
sort_keys=False,
)explicit_start=True writes --- before every document, which makes stream boundaries visible to other YAML tools.
Fail when a mapping repeats a key reject-duplicate-keys
import yaml
from yaml.constructor import ConstructorError
from yaml.resolver import BaseResolver
class UniqueKeyLoader(yaml.SafeLoader):
pass
def construct_unique_mapping(loader, node, deep=False):
loader.flatten_mapping(node)
mapping = {}
for key_node, value_node in node.value:
key = loader.construct_object(key_node, deep=deep)
if key in mapping:
raise ConstructorError(
'while constructing a mapping', node.start_mark,
f'duplicate key: {key!r}', key_node.start_mark,
)
mapping[key] = loader.construct_object(value_node, deep=deep)
return mapping
UniqueKeyLoader.add_constructor(
BaseResolver.DEFAULT_MAPPING_TAG,
construct_unique_mapping,
)
config = yaml.load(text, Loader=UniqueKeyLoader)PyYAML 6.0.3 normally keeps the later duplicate value. This SafeLoader subclass turns that silent overwrite into ConstructorError.
Parse one application-specific tag add-safe-constructor
import dataclasses
import yaml
@dataclasses.dataclass(frozen=True)
class Endpoint:
url: str
class AppLoader(yaml.SafeLoader):
pass
def endpoint_constructor(loader, node):
return Endpoint(loader.construct_scalar(node))
AppLoader.add_constructor('!endpoint', endpoint_constructor)
value = yaml.load('api: !endpoint https://api.example.com', Loader=AppLoader)Register the tag on a SafeLoader subclass so the global loader is not changed for unrelated YAML input.
Emit an application-specific tag add-safe-representer
class AppDumper(yaml.SafeDumper):
pass
def endpoint_representer(dumper, value):
return dumper.represent_scalar('!endpoint', value.url)
AppDumper.add_representer(Endpoint, endpoint_representer)
text = yaml.dump({'api': Endpoint('https://api.example.com')}, Dumper=AppDumper)A matching constructor is required when reading !endpoint back. SafeLoader does not accept an unknown custom tag by itself.
Inspect the representation tree without constructing objects inspect-node-tags
import yaml
node = yaml.compose(text, Loader=yaml.SafeLoader)
if node is not None:
print(node.tag, node.start_mark.line + 1)compose() returns ScalarNode, SequenceNode, or MappingNode objects with tags and source marks. It does not return normal Python values.
Report a useful YAML location catch-parse-error
import yaml
try:
config = yaml.safe_load(text)
except yaml.YAMLError as error:
mark = getattr(error, 'problem_mark', None)
if mark is None:
raise
raise ValueError(
f'YAML error at line {mark.line + 1}, column {mark.column + 1}'
) from errorParser errors may carry a zero-based problem_mark. Some YAMLError subclasses do not, so check before reading line and column.
Keep YAML 1.1 words as strings quote-ambiguous-scalars
feature_state: 'on'
confirmation: 'yes'
country_code: 'NO'PyYAML 6.0.3 resolves plain on, yes, and NO as booleans. Quotes force these values to remain strings.
Check the data shape after parsing validate-loaded-shape
import yaml
from pydantic import BaseModel, ConfigDict
class ServiceConfig(BaseModel):
model_config = ConfigDict(extra='forbid')
host: str
port: int
raw = yaml.safe_load(text)
config = ServiceConfig.model_validate(raw)safe_load() controls which tags can construct values; it does not enforce required keys, field types, or unknown-key policy.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| ruamel.yaml | PyPI | Use it when an editor must retain comments, quoting, and other source details across a YAML round trip. |
| strictyaml | PyPI | Use it when configuration should obey an explicit schema and avoid much of YAML's implicit typing and tag behavior. |
| tomli | PyPI | Use it on Python before 3.11 when a read-only TOML parser covers a configuration format you control. |
More utils guides
lru-cache · type-fest · ajv · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

