YAML isn't in the standard library, but it's everywhere in professional Python work — Docker Compose, Kubernetes manifests, CI pipeline configs, and application settings files are almost always YAML. This lesson covers using PyYAML correctly, including a security detail that has caused real production incidents.
1Why YAML, Despite Not Being in the Standard Library
YAML deliberately optimizes for human readability and hand-editing in ways JSON does not: comments (#) are supported natively, strings generally don't require quotes, and structure is expressed through indentation rather than braces and brackets — the difference between a config file that reads naturally to a human maintaining it by hand and one that's technically equivalent but visually noisier. This is precisely why YAML has become the de facto standard for configuration formats meant to be directly edited by engineers: Docker Compose files, Kubernetes manifests, GitHub Actions workflows, and countless application configuration files are all YAML specifically for this readability reason.
The trade-off is that YAML's specification is considerably larger and more complex than JSON's — it supports multiple ways to express the same data (flow style vs block style, multiple string quoting conventions, anchors and references for avoiding repetition), and that complexity is part of what makes it comfortable for humans while also being the source of the security consideration covered next.
PyYAML (the dominant third-party library for YAML in Python — not standard library, requiring pip install pyyaml) is the practical tool for working with it, and its API deliberately mirrors the json module's shape (load/dump, loads/dumps-equivalents) specifically to make the two feel familiar and interchangeable for anyone who already knows json.
# config.yaml
app_name: My Service # comments are supported
debug: false
max_connections: 100
allowed_hosts:
- localhost
- example.com
database:
host: db.internal
port: 5432Optimized for hand-editing, unlike JSON
2safe_load() vs load(): A Real Security Distinction, Not Just an API Preference
PyYAML's full YAML support includes a tagging mechanism (!!python/object:...) that can instruct the parser to construct arbitrary Python objects during loading — including, in documented, real-world exploited cases, objects whose construction has side effects capable of executing arbitrary code on the machine parsing the file. yaml.load(), called without explicitly specifying a restricted Loader, uses the full, unrestricted loader by default in older PyYAML versions, meaning parsing an untrusted YAML file with it can be a genuine remote code execution vulnerability, not a theoretical concern — this exact issue has caused real, documented CVEs in production software.
yaml.safe_load() restricts parsing to only the standard, 'safe' YAML data types — strings, numbers, booleans, lists, and dictionaries — with no ability to construct arbitrary Python objects or trigger code execution as a side effect of parsing, regardless of what the input file contains. For the overwhelming majority of real use cases — loading a configuration file, parsing structured data — this is all the functionality actually needed, and it eliminates the security risk entirely rather than requiring careful trust judgments about the input's origin.
The unconditional rule this establishes: use yaml.safe_load(), never yaml.load() without an explicitly safe Loader argument, for any YAML input whose origin isn't fully and permanently trusted — which, in practice, should be treated as the default assumption for essentially all YAML parsing, the same way you'd treat any other externally-sourced input as untrusted by default.
import yaml
# SAFE -- only handles standard data, cannot execute anything
with open("config.yaml") as f:
config = yaml.safe_load(f)
# DANGEROUS -- can be tricked into executing arbitrary code from untrusted YAML
# yaml.load(f) -- without an explicit safe Loader, avoid this entirelyRestricted to safe types — no arbitrary object construction, no code execution risk
3Writing YAML Back Out: Controlling Readability
yaml.safe_dump(data, f, default_flow_style=False, sort_keys=False) writes a Python data structure back to YAML, with two options that directly affect how readable and maintainable the resulting file is for a human who'll later hand-edit it. default_flow_style=False produces YAML in indentation-based block style (matching the readable config.yaml shown earlier) rather than the more compact but visually denser flow style ({app_name: My Service, debug: false}, closer to inline JSON) — block style is almost always the right choice for a file humans are expected to read and edit directly.
sort_keys=False preserves the dict's original key order in the output, rather than PyYAML's default behavior of alphabetically sorting keys — this matters because alphabetical sorting can scramble a logical, intentional grouping (putting related settings near each other) into an arbitrary alphabetical order that's genuinely harder for a human to scan and understand, even though it's technically equivalent data.
The general principle these two options illustrate: when YAML output is meant for humans (which is the entire reason to choose YAML over JSON in the first place), the specific formatting choices — block vs flow style, preserved vs sorted key order — meaningfully affect the format's actual value proposition. Writing YAML with defaults that happen to produce dense, alphabetically-scrambled output defeats much of the purpose of choosing YAML over a simpler, more compact format like JSON to begin with.
import yaml
data = {"app_name": "My Service", "debug": False, "max_connections": 100}
with open("config.yaml", "w") as f:
yaml.safe_dump(data, f, default_flow_style=False, sort_keys=False)
# default_flow_style=False -- writes block style (readable), not {inline: style}Block style, original key order — genuinely human-editable
4Step-by-Step Breakdown
yaml.load() has an option that can execute arbitrary code from a YAML file. If that sounds dangerous, it is — let's use the function that isn't.
YAML reads more naturally than JSON for humans -- no mandatory quotes on strings, comments are supported, and indentation replaces braces.
yaml.safe_load() parses standard YAML data types ONLY -- strings, numbers, lists, dicts. yaml.load() (without Loader=SafeLoader) can construct ARBITRARY Python objects, including a security risk.
Checkpoint: Why is yaml.load() (without specifying a safe Loader) considered dangerous on untrusted YAML input?
- →YAML's full tag syntax can instruct the default loader to construct arbitrary Python objects, which can be exploited to execute arbitrary code
- →It is simply much slower than yaml.safe_load(), with no other real difference
yaml.safe_dump() writes Python data back to YAML -- with options controlling formatting for human readability.
Checkpoint: What does default_flow_style=False do in yaml.safe_dump()?
- →It writes YAML in indented block style (more human-readable) rather than compact inline {} / [] style
- →It validates the data against a schema before writing
YAML handles human-edited configuration; XML is the next format, still common in enterprise and legacy system integration.
Safely Parse Real YAML. Finish parse_config(): safe_load() never executes arbitrary code from untrusted input.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Best Practices
Always use yaml.safe_load(), never yaml.load() without an explicit safe Loader
This is not a style preference — yaml.load()'s default behavior in many PyYAML versions can construct arbitrary Python objects from untrusted input, a documented, exploitable security risk that safe_load() eliminates entirely.
Use default_flow_style=False when writing YAML meant for humans to read or edit
It produces the readable, indentation-based block style matching hand-written YAML conventions, rather than a denser, less scannable inline format.
Frequent Bugs
Using yaml.load() (without an explicit safe Loader argument) to parse YAML from a user upload, API request, or any other untrusted source, creating a real code-execution security vulnerability.
Always use yaml.safe_load() for any YAML input that is not fully and permanently trusted — which should be the default assumption for essentially all YAML parsing in production code.
Real-World Examples
Loading Application Configuration From a YAML File
An application reads its configuration (database connection, feature flags, logging level) from a YAML file at startup, edited directly by engineers and DevOps.
import yaml
def load_config(path: str) -> dict:
with open(path, encoding="utf-8") as f:
return yaml.safe_load(f)
config = load_config("config.yaml")
print(config["database"]["host"])
print(config["allowed_hosts"])