Skip to content
datahacks.dev
YAML

Writing YAML From Python Without Ugly Output or Surprises

PyYAML safe_dump sorts keys, escapes Unicode and quotes multiline strings with blank lines. The options and a block-string representer that fix each default.

6 min read
Summarize in AI
Flat illustration of a tree of nested objects beside the document it came from, with the YAML logo.

Writing YAML from code looks easy: yaml.dump(data). The first output usually surprises people. The keys are in alphabetical order, accented characters are escaped into \u sequences, a two-line string becomes a quoted scalar with blank lines in the middle, and the style of lists differs from what their editor produces. None of it is wrong YAML. It is YAML that nobody would write by hand. This post shows each default and the option that changes it, with the real output.

The defaults

Python
cfg = {"name": "web", "ports": [80, 443],
       "env": {"MODE": "prod", "NOTE": "line one\nline two\n"},
       "title": "Café — menu", "zeta": 1, "alpha": 2}
print(yaml.safe_dump(cfg), end="")
Text
alpha: 2
env:
  MODE: prod
  NOTE: 'line one

    line two

    '
name: web
ports:
- 80
- 443
title: "Caf\xE9 — menu"
zeta: 1

Five things to notice. Keys are sorted: alpha came first and zeta last, because sort_keys defaults to True. The multiline value became a single-quoted scalar with blank lines representing the line breaks, which is valid and unreadable. Non-ASCII characters were escaped, "Caf\xE9 — menu". List items sit at the same indentation as their key (ports: then - 80 at column 0). And the whole document is block style, which is the default for nested data (only the simple case of an all-scalar collection might use flow style, depending on default_flow_style, whose default changed over versions).

The options

Python
yaml.safe_dump(cfg, sort_keys=False, allow_unicode=True, default_flow_style=False, indent=2)
  • **sort_keys=False** keeps the dictionary's insertion order, so the output follows the order you built the data in. Python 3.7 and later preserve insertion order, so this is reliable.

  • **allow_unicode=True** writes the real characters. Make sure you open the output file with encoding="utf-8".

  • **default_flow_style=False** forces block style everywhere, so no {a: 1} or [1, 2] appears for nested collections. Set it explicitly: its default has varied.

  • **indent=2** sets the indentation of nested mappings. (PyYAML does not indent lists under their key with this option.)

  • **width=1000** stops PyYAML from wrapping long strings at 80 columns, which can introduce line folding in plain scalars that readers find surprising.

Multiline strings as blocks

To get readable block scalars for multiline text, tell the dumper how to represent a marker type. A subclass of str is a clean marker, so only strings you mark are affected:

Python
class Lit(str): pass
yaml.SafeDumper.add_representer(
    Lit, lambda d, s: d.represent_scalar("tag:yaml.org,2002:str", s, style="|"))

cfg["env"]["NOTE"] = Lit("line one\nline two\n")
print(yaml.safe_dump(cfg, sort_keys=False, allow_unicode=True, default_flow_style=False, indent=2), end="")
Text
name: web
ports:
- 80
- 443
env:
  MODE: prod
  NOTE: |
    line one
    line two
title: Café — menu
zeta: 1
alpha: 2

The key order is the one you built, the accents are real, and the note is a pipe block. The representer is registered on SafeDumper, so safe_dump uses it, and it only applies to Lit values, so ordinary strings are unchanged. If you want every multiline string to be a block, register a representer for str that checks for a newline and picks the style. Be aware that block style is not always possible: PyYAML falls back to a quoted style when a string has trailing spaces on a line or other characters that a literal block cannot hold, and it does so silently.

List indentation

PyYAML always writes list items at the indentation of the parent key (ports: followed by - 80 at the same column). It is valid and common in some ecosystems, but many style guides and linters prefer the items indented. ruamel.yaml can do either:

Python
from ruamel.yaml import YAML
y = YAML(); y.indent(mapping=2, sequence=4, offset=2)
y.dump({"a": [1, 2], "b": [{"x": 1}]}, sys.stdout)
Text
a:
  - 1
  - 2
b:
  - x: 1

sequence=4, offset=2 means the dash sits two columns in and the content four. That is the layout most editors and Kubernetes examples use. If you need to produce files that pass a linter with indent-sequences: true, this is the library. yamllint with indent-sequences: consistent accepts both, as long as you choose one.

What else the library does for you

Letting the library write the file prevents a class of bugs, because it knows which strings need quotes. A value like NO, 12:30 or 3.10 that is a Python string is written with quotes so that a YAML 1.1 reader gets a string back; a Python True is written true; a None is null; a date object is written as a date. Compare with building text by hand:

Python
f"country: {code}\nversion: {version}\n"

With code="NO" and version="3.10" this writes country: NO and version: 3.10, which read back as False and 3.1. The library would have quoted both. Hand-built YAML breaks on the first value with a colon, a hash, a leading special character or a boolean-looking word. The rule: build data, then dump. Write YAML by hand only for a fixed template with no variable values, and test it.

Round trip: dump what you loaded

If you load a file, change it and write it back, PyYAML loses the comments and normalizes the layout:

Text
service:
  name: web
  ports:
  - 80
  - 443
  tags:
  - a
  - b
  note: 'keep this

    block

    '
  retries: 3

That was the output of safe_dump(safe_load(file)): the comment is gone, the flow list became a block list, the block scalar became a quoted blob and the quotes around "web" are gone. Use a round-trip library for files people maintain (preserving comments and order with ruamel yaml).

Checking the output

Reload what you wrote and compare it with the data you started from: assert yaml.safe_load(output) == cfg. The assertion catches representer mistakes and type changes (a string that came back as a number). For files that other tools read, also run the linter on the output, and load it with the second parser those tools use.

The cluster hub is reading and writing YAML in Python safely. For reading, see loading YAML in Python with PyYAML safe_load. Block scalar styles are covered in YAML multiline strings with pipe and greater than. The dump options are documented in the PyYAML documentation and ruamel.yaml's in its reference. Reformat output in the YAML formatter.

When PyYAML is the wrong tool for writing YAML

The file must keep comments or a hand-made layout. Use ruamel.yaml round-trip mode, or edit with yq.

You need YAML 1.2 output. PyYAML writes YAML 1.1 and quotes accordingly. A 1.2 reader is fine with the extra quotes.

The output is huge. The pure Python dumper is slow on large structures. Use the C dumper (CSafeDumper) when available, or a different format for bulk data.

The consumer wants exact formatting. Some tools diff the file byte by byte. Pin the dumper options, test the output and keep the dumper version stable.

Also inScripting
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.