Converting a Python dictionary to YAML takes two lines with PyYAML: install it, then call yaml.dump(). The details that matter are the formatting arguments — without them you get flow style, sorted keys and escaped Unicode, none of which is what people expect — and using safe_load rather than load when reading back.
Install and convert#
python -m pip install pyyaml
import yaml
config = {
"app": "shop",
"debug": False,
"port": 8080,
"database": {"host": "localhost", "name": "shop_db"},
"features": ["search", "cart", "checkout"],
}
print(yaml.dump(config, sort_keys=False))
app: shop
debug: false
port: 8080
database:
host: localhost
name: shop_db
features:
- search
- cart
- checkout
The package is pyyaml but the import is yaml — a mismatch worth remembering.
The arguments worth setting#
text = yaml.dump(
config,
sort_keys=False, # keep your key order
default_flow_style=False, # block style, not inline braces
allow_unicode=True, # real characters, not escapes
indent=2,
width=80,
)
What each fixes:
- sort_keys defaults to
True, which alphabetises everything and destroys any deliberate grouping. - default_flow_style controls whether nested data is written inline. Modern PyYAML defaults to block style, but being explicit costs nothing.
- allow_unicode is the one people miss. Without it, accented characters come out as escape sequences.
data = {"city": "M\u00fcnchen", "note": "caf\u00e9"}
print(yaml.dump(data)) # escaped, hard to read
print(yaml.dump(data, allow_unicode=True)) # city: München
Indenting lists properly#
PyYAML puts list dashes at the parent’s indent level by default. Many people prefer them indented:
class IndentedDumper(yaml.SafeDumper):
def increase_indent(self, flow=False, indentless=False):
return super().increase_indent(flow, False)
print(yaml.dump(config, Dumper=IndentedDumper, sort_keys=False))
features:
- search
- cart
- checkout
Both forms are valid YAML and parse identically. This is purely about how it reads.
Writing to a file#
from pathlib import Path
with open("config.yaml", "w", encoding="utf-8") as f:
yaml.dump(config, f, sort_keys=False, allow_unicode=True)
# or via pathlib
Path("config.yaml").write_text(
yaml.dump(config, sort_keys=False, allow_unicode=True),
encoding="utf-8",
)
Always specify encoding="utf-8". Without it, the platform default is used, and on some Windows configurations that cannot represent the characters allow_unicode just preserved.
Reading it back#
with open("config.yaml", encoding="utf-8") as f:
loaded = yaml.safe_load(f)
print(loaded["database"]["host"])
For a file containing several documents separated by ---:
for document in yaml.safe_load_all(text):
print(document)
Types that need attention#
import datetime
data = {
"when": datetime.date(2026, 9, 6), # written as a native YAML date
"pair": (1, 2), # tuple - needs care
"unique": {1, 2, 3}, # set - needs care
"nothing": None, # written as null
}
print(yaml.dump(data, default_flow_style=False))
Dates and None are handled natively. Tuples and sets are written using PyYAML-specific tags that other YAML parsers will not understand, and safe_load will refuse to read back. Convert them first:
def yaml_ready(obj):
if isinstance(obj, dict):
return {str(k): yaml_ready(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple, set)):
return [yaml_ready(v) for v in obj]
return obj
print(yaml.dump(yaml_ready(data), sort_keys=False))
Note that a tuple becomes a list on the way out, so it will not come back as a tuple. YAML has no tuple concept, and that is fine for configuration.
Custom objects#
from dataclasses import dataclass, asdict
@dataclass
class Server:
host: str
port: int
servers = [Server("a.example.com", 80), Server("b.example.com", 8080)]
print(yaml.dump([asdict(s) for s in servers], sort_keys=False))
Converting to plain dictionaries first keeps the output portable. PyYAML can serialise arbitrary objects with tags, but the result is only readable by PyYAML, which defeats most of the reason for choosing YAML.
Multi-line strings#
data = {"description": "First line\nSecond line\nThird line"}
print(yaml.dump(data, default_style="|"))
description: |-
First line
Second line
Third line
The pipe character introduces a literal block, which preserves the line breaks. It is much more readable than a single long line full of escape sequences.
YAML or JSON?#
| YAML | JSON | |
|---|---|---|
| Comments | Yes | No |
| Written by hand | Comfortable | Awkward |
| In the standard library | No | Yes |
| Whitespace sensitive | Yes | No |
| Best for | Configuration | Data interchange |
The short version: YAML for files people edit, JSON for data programs exchange.
Questions people ask#
Is YAML part of the standard library?
No. PyYAML is a third-party package. JSON and tomllib are built in; YAML is not.
Why is my key order alphabetical?
sort_keys defaults to True. Pass sort_keys=False to keep the order of your dictionary.
What about ruamel.yaml?
It supports newer YAML versions and, importantly, preserves comments and formatting when you load and re-save a file. Worth using if you are editing files people also maintain by hand.
How do I add comments to generated YAML?
PyYAML cannot. Either write the header yourself before dumping, or use ruamel.yaml, which supports comments properly.
Where to go next#
- Python dictionaries explained — the structure being converted.
- Python file handling — reading and writing with the right encoding.
- The Python standard library — the built-in formats you may not need YAML for.