Skip to content

Dumping YAML

Dumping is the act of turning native Python objects back into YAML. dumps takes any supported object and returns the encoded document. Its companion dump writes to a file or file-like target instead. Both share the same type rules and the same emitting options, so what you learn here applies to either.

dumps returns bytes, not str. The bytes are UTF-8 encoded and end with a trailing newline:

import yamlrocks
yamlrocks.dumps({"name": "app", "ports": [80, 443]})
# b'name: app\nports:\n - 80\n - 443\n'

Returning bytes is a deliberate, performance-minded choice: most destinations for a serialized document (a file opened in binary mode, a socket, an HTTP response body, a subprocess stdin) want bytes anyway, so handing you bytes avoids an extra encode step.

The shape of the output is controlled with option flags, combined with |. Each flag below is shown with a runnable before-and-after so you can see exactly what changes.

Indentation: OPT_INDENT_2 and OPT_INDENT_4

Section titled “Indentation: OPT_INDENT_2 and OPT_INDENT_4”

Block indentation defaults to two spaces (OPT_INDENT_2). Pass OPT_INDENT_4 for four:

import yamlrocks
data = {"server": {"host": "localhost", "port": 8080}}
yamlrocks.dumps(data)
# b'server:\n host: localhost\n port: 8080\n'
yamlrocks.dumps(data, option=yamlrocks.OPT_INDENT_4)
# b'server:\n host: localhost\n port: 8080\n'

Sequence indentation: OPT_INDENTLESS_SEQUENCES

Section titled “Sequence indentation: OPT_INDENTLESS_SEQUENCES”

A block sequence under a key is indented one level by default (key: then - item), the style most configuration ecosystems use. Pass OPT_INDENTLESS_SEQUENCES to align the dashes with the key instead (key: then - item), the “indentless” style favored by kubectl and much of the Kubernetes world:

import yamlrocks
data = {"ports": [80, 443]}
yamlrocks.dumps(data)
# b'ports:\n - 80\n - 443\n'
yamlrocks.dumps(data, option=yamlrocks.OPT_INDENTLESS_SEQUENCES)
# b'ports:\n- 80\n- 443\n'

By default keys are emitted in insertion order. OPT_SORT_KEYS sorts every mapping alphabetically, which is handy for stable diffs and reproducible output:

import yamlrocks
yamlrocks.dumps({"b": 1, "a": 2})
# b'b: 1\na: 2\n'
yamlrocks.dumps({"b": 1, "a": 2}, option=yamlrocks.OPT_SORT_KEYS)
# b'a: 2\nb: 1\n'

The default is block style, where each entry sits on its own line. OPT_FLOW_STYLE emits the compact JSON-like flow form with {} and []:

import yamlrocks
yamlrocks.dumps({"a": [1, 2]})
# b'a:\n - 1\n - 2\n'
yamlrocks.dumps({"a": [1, 2]}, option=yamlrocks.OPT_FLOW_STYLE)
# b'{a: [1, 2]}\n'

A multi-line string is emitted as a literal | block by default, which is how real-world YAML overwhelmingly writes multi-line content (embedded scripts, certificates, descriptions) and far more readable than a double-quoted scalar full of \n escapes:

import yamlrocks
yamlrocks.dumps({"s": "line 1\nline 2\n"})
# b's: |\n line 1\n line 2\n'

The chomping indicator is chosen automatically so the block round-trips exactly: | keeps a single trailing newline, |- strips it when there is none, and |+ keeps extra trailing blank lines. A string a literal block cannot represent faithfully (it contains a carriage return or other control character, or its first line begins with whitespace) falls back to a double-quoted scalar, so loads(dumps(x)) == x holds for every string.

Document markers: OPT_EXPLICIT_START and OPT_EXPLICIT_END

Section titled “Document markers: OPT_EXPLICIT_START and OPT_EXPLICIT_END”

These add the explicit --- start marker and ... end marker. They are useful when concatenating documents into a single stream:

import yamlrocks
yamlrocks.dumps({"a": 1}, option=yamlrocks.OPT_EXPLICIT_START | yamlrocks.OPT_EXPLICIT_END)
# b'---\na: 1\n...\n'

None is left blank by default (key: with nothing after the colon), which is what hand-written configs and PyYAML-based tools overwhelmingly produce. Some formats prefer the explicit null keyword (data and spec formats such as OpenAPI), and some prefer the ~ indicator. Pass null_style to choose per call, or set OPT_NULL_AS_KEYWORD / OPT_NULL_AS_TILDE to make that style the default (the two flags are mutually exclusive):

import yamlrocks
yamlrocks.dumps({"a": None, "b": None})
# b'a:\nb:\n'
yamlrocks.dumps({"a": None, "b": None}, null_style="null")
# b'a: null\nb: null\n'
yamlrocks.dumps({"a": None}, null_style="~")
# b'a: ~\n'
yamlrocks.dumps({"a": None}, option=yamlrocks.OPT_NULL_AS_KEYWORD)
# b'a: null\n'

The three styles all parse back to None, so the choice is cosmetic. The blank form is only used where it is unambiguous, a block mapping value or a block sequence entry; at the top level, inside a flow collection, or as a mapping key it falls back to null so the output stays valid YAML 1.2. The null_style argument overrides the flag for a single call.

By default dumps never wraps: a long scalar or flow collection emits on one line. Pass width=N to fold lines to a best-effort maximum, the way PyYAML’s width does. Plain and quoted scalars fold at spaces, and flow collections break after commas:

import yamlrocks
config = {"description": "a fairly long sentence that we would like wrapped onto a few lines"}
yamlrocks.dumps(config, width=40)
# b'description: a fairly long sentence that\n we would like wrapped onto a few lines\n'

The one rule that is never broken is value fidelity: a fold only happens where it cannot change the decoded string, so loads(dumps(x, width=N)) == x always holds. That makes the width a soft limit, because some lines have no safe place to break:

  • A run of two or more spaces is never split (a fold there would drop one space).
  • A long word with no spaces (a URL, a token) stays on its line.
  • A multi-line string emits as a literal | block, whose lines are preserved verbatim and so are not re-wrapped.
import yamlrocks
yamlrocks.dumps({"url": "https://example.com/a/very/long/unbreakable/path/here"}, width=20)
# b'url: https://example.com/a/very/long/unbreakable/path/here\n'

This is the knob to reach for when a project requires lines at or under a length (for example to satisfy yamllint’s line-length rule, whose default also exempts a single unbreakable word).

width applies only to the fast dumps path. Round-trip mode preserves the original layout byte-for-byte, so it is unaffected.

YAMLRocks quotes scalars only when needed to keep the document unambiguous. A string that would otherwise parse back as another type (a bool, a number, null) is quoted automatically, so a round-trip never silently changes a value:

import yamlrocks
yamlrocks.dumps({"version": "1.0", "flag": "yes"})
# b'version: "1.0"\nflag: "yes"\n'

Here 1.0 is quoted so it stays a string rather than becoming the float 1.0, and yes is quoted so it is not mistaken for a YAML 1.1 boolean by downstream tools. See YAML 1.1 vs 1.2 for why that matters.

Quoting uses double quotes by default. Pass OPT_SINGLE_QUOTES to use single quotes instead, which avoid backslash escaping for values that contain many backslashes (a regex or a Windows path, say); a value that cannot be single-quoted (it contains a line break) still falls back to double quotes.

import yamlrocks
yamlrocks.dumps({"flag": "yes"}, option=yamlrocks.OPT_SINGLE_QUOTES)
# b"flag: 'yes'\n"

Beyond the core YAML types, YAMLRocks serializes a set of common Python types directly. The table below lists the built-in mapping:

Python typeYAML outputNotes
dictmappingkeys in insertion order, or sorted with OPT_SORT_KEYS
list, tuplesequence
strscalarquoted only when needed
int, floatscalar
booltrue / false
Nonenull
datetimeISO 8601 timestampsee datetime options below
date'YYYY-MM-DD'
timeHH:MM:SS[.ffffff]
uuid.UUIDscalar stringunquoted; a UUID is not a YAML number
decimal.Decimalscalarexact, no float rounding
enum.Enumthe member’s value
dataclass instancemapping of fields
pathlib.Pathscalar string
numpy array / scalarsequence / scalarrequires OPT_SERIALIZE_NUMPY

A few of these are worth seeing in action:

import yamlrocks
import uuid
import decimal
import enum
import pathlib
from dataclasses import dataclass
yamlrocks.dumps({"id": uuid.UUID("12345678-1234-5678-1234-567812345678")})
# b'id: 12345678-1234-5678-1234-567812345678\n'
yamlrocks.dumps({"price": decimal.Decimal("3.14")})
# b'price: 3.14\n'
class Color(enum.Enum):
GREEN = "green"
yamlrocks.dumps({"color": Color.GREEN})
# b'color: green\n'
@dataclass
class Point:
x: int
y: int
yamlrocks.dumps(Point(1, 2))
# b'x: 1\ny: 2\n'
yamlrocks.dumps({"path": pathlib.Path("/etc/app/config.yaml")})
# b'path: /etc/app/config.yaml\n'

A timezone-aware datetime serializes to a full ISO 8601 timestamp by default:

import yamlrocks
import datetime
dt = datetime.datetime(
2026, 6, 5, 12, 30, 45, 123456, tzinfo=datetime.timezone.utc
)
yamlrocks.dumps(dt)
# b'2026-06-05T12:30:45.123456+00:00\n'

Three flags adjust how timestamps render:

  • OPT_OMIT_MICROSECONDS drops the microsecond component.
  • OPT_NAIVE_UTC treats a naive datetime (no tzinfo) as UTC and appends the +00:00 offset.
  • OPT_UTC_Z renders a +00:00 offset as the shorter Z.
import yamlrocks
import datetime
dt = datetime.datetime(
2026, 6, 5, 12, 30, 45, 123456, tzinfo=datetime.timezone.utc
)
yamlrocks.dumps(dt, option=yamlrocks.OPT_OMIT_MICROSECONDS | yamlrocks.OPT_UTC_Z)
# b'2026-06-05T12:30:45Z\n'
naive = datetime.datetime(2026, 6, 5, 12, 30, 45)
yamlrocks.dumps(naive)
# b'2026-06-05T12:30:45\n'
yamlrocks.dumps(naive, option=yamlrocks.OPT_NAIVE_UTC | yamlrocks.OPT_UTC_Z)
# b'2026-06-05T12:30:45Z\n'

numpy support is off by default, so an unflagged numpy value is treated as an unsupported type:

import yamlrocks
import numpy as np
yamlrocks.dumps({"a": np.array([1, 2])})
# yamlrocks.YAMLRocksUnserializableError: type ndarray is not YAML serializable

Pass OPT_SERIALIZE_NUMPY to serialize arrays and scalars:

import yamlrocks
import numpy as np
yamlrocks.dumps({"a": np.array([1, 2])}, option=yamlrocks.OPT_SERIALIZE_NUMPY)
# b'a:\n - 1\n - 2\n'
yamlrocks.dumps({"n": np.int64(5)}, option=yamlrocks.OPT_SERIALIZE_NUMPY)
# b'n: 5\n'

When YAMLRocks meets a value it does not know how to serialize, it calls your default callback with that value. Return something serializable and YAMLRocks emits that instead:

import yamlrocks
yamlrocks.dumps(
{"point": complex(1, 2)},
default=lambda o: [o.real, o.imag] if isinstance(o, complex) else o,
)
# b'point:\n - 1.0\n - 2.0\n'

The callback can return a nested structure, and YAMLRocks will serialize that in turn, so you can map a custom object onto a mapping or sequence:

import yamlrocks
class Money:
def __init__(self, amount, currency):
self.amount = amount
self.currency = currency
def encode(obj):
if isinstance(obj, Money):
return {"amount": obj.amount, "currency": obj.currency}
raise TypeError
yamlrocks.dumps({"total": Money(42, "EUR")}, default=encode)
# b'total:\n amount: 42\n currency: EUR\n'

If no default is given, or default raises, or it returns a value that is itself unsupported, YAMLRocks raises YAMLRocksEncodeError. This is a subclass of TypeError, so existing except TypeError handlers keep working:

import yamlrocks
yamlrocks.dumps({"x": object()})
# yamlrocks.YAMLRocksUnserializableError: type object is not YAML serializable

Passthrough: route built-in types to default

Section titled “Passthrough: route built-in types to default”

Sometimes you want to override how a type YAMLRocks already supports is emitted. The passthrough flags tell YAMLRocks to skip its built-in handling for a type and send it to default instead.

OPT_PASSTHROUGH_DATETIME routes datetime, date, and time to default:

import yamlrocks
import datetime
yamlrocks.dumps(
datetime.date(2026, 6, 5),
option=yamlrocks.OPT_PASSTHROUGH_DATETIME,
default=lambda o: o.strftime("%d/%m/%Y"),
)
# b'05/06/2026\n'

OPT_PASSTHROUGH_DATACLASS routes dataclass instances to default, so you can emit them in a custom shape instead of a field mapping:

import yamlrocks
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
yamlrocks.dumps(
Point(1, 2),
option=yamlrocks.OPT_PASSTHROUGH_DATACLASS,
default=lambda o: [o.x, o.y],
)
# b'- 1\n- 2\n'

dump is the file-oriented counterpart to dumps. Give it a path or an open file object as the target. It takes the same default and option arguments:

import yamlrocks
yamlrocks.dump({"name": "app", "port": 8080}, "config.yaml")
with open("config.yaml", "wb") as f:
yamlrocks.dump({"name": "app", "port": 8080}, f)

For round-trip documents loaded from disk, dump(doc) with no target writes only the files that actually changed, including any split-out includes. See round-trip editing and includes for the dump_includes helpers that go with that workflow.

dump has an async counterpart, async_dump, which writes a file off the event loop. It takes the same arguments and runs the serialize-and-write in a worker thread, so a slow disk does not stall an asyncio application:

import asyncio
import yamlrocks
async def main():
await yamlrocks.async_dump({"name": "app", "port": 8080}, "config.yaml")
asyncio.run(main())
with open("config.yaml") as f:
f.read()
# 'name: app\nport: 8080\n'

async_dump exists only because it does file I/O. There is deliberately no async_dumps and no async_to_json. Unlike parsing (where the native scan releases the GIL and genuinely runs off the loop thread), serializing must walk the Python object graph, and that traversal holds the GIL the whole time. Moving it to a worker thread buys little, because the worker still cannot run in parallel with the loop while it holds the GIL.

On the rare occasion you do need an in-memory serialize off the loop, wrap the synchronous call yourself:

import asyncio
import yamlrocks
async def main():
return await asyncio.to_thread(yamlrocks.dumps, {"name": "app"})
asyncio.run(main())
# b'name: app\n'

The same reasoning and workaround apply to JSON export; see the JSON guide. For the load side, where async genuinely runs off the loop, see async loading.