json.dumps() ensure_ascii Unicode Escapes (tested)

By default, json.dumps() replaces every non-ASCII character with a \uXXXX escape sequence, even though JSON's spec allows raw UTF-8 text directly. This is a deliberate default, not a bug, and it's controlled by one argument. We measured exactly what changes when you flip it.

1. The default behavior

import json

json.dumps({"name": "café", "city": "東京"})
'{"name": "caf\\u00e9", "city": "\\u6771\\u4eac"}'

Measured: every character outside the ASCII range (café's é, and both characters of Tokyo's name in Japanese) gets replaced by its \uXXXX escape. This is ensure_ascii=True, the default value of that parameter — the output is still exactly one JSON string, just restricted to ASCII bytes.

2. ensure_ascii=False writes the real characters

json.dumps({"name": "café", "city": "東京"}, ensure_ascii=False)
'{"name": "café", "city": "東京"}'

Measured: with ensure_ascii=False, the raw UTF-8 characters are written directly into the string instead of being escaped. Both forms are equally valid JSON per the spec — escaping non-ASCII characters is optional, not a compliance requirement.

3. Both forms parse back to the identical data

a = json.dumps({"name": "café"})
b = json.dumps({"name": "café"}, ensure_ascii=False)
json.loads(a) == json.loads(b)
True

Measured: this setting only affects how the text looks on the wire or on disk — it has zero effect on the actual data once decoded. Any compliant JSON parser handles both forms identically, so switching it is always safe for interoperability.

4. The real tradeoff: byte size and readability

len(a.encode("utf-8"))   # ensure_ascii=True output
45
len(b.encode("utf-8"))   # ensure_ascii=False output
35

Measured: on this small example, the escaped form was actually larger in bytes — each \uXXXX escape is 6 ASCII bytes, while the real UTF-8 character it replaces is often 2-4 bytes. For text with a lot of non-ASCII content, ensure_ascii=False produces smaller output as well as output that's actually readable in a log file or a raw HTTP response, instead of a wall of escape codes.

5. When to use which

Use ensure_ascii=False whenever you control both ends (your own API, your own log files, writing to a UTF-8-aware file or terminal) — it's smaller and human-readable. Keep the default ensure_ascii=True only if you have a specific reason to guarantee pure-ASCII output, such as embedding the JSON string inside another format that can't safely carry raw UTF-8 bytes, or a downstream system with unknown or unreliable encoding handling.

Frequently asked questions

Why does json.dumps() turn café into caf\u00e9 by default?

We confirmed this is the default value of the ensure_ascii parameter (True) — it deliberately escapes every non-ASCII character to a \\uXXXX sequence so the output contains only ASCII bytes, even though JSON's spec allows raw UTF-8 text directly.

Does ensure_ascii=False produce invalid JSON?

No — we measured that both the escaped and unescaped forms are equally valid per the JSON spec, and both parse back via json.loads() to the identical Python data; escaping non-ASCII characters is optional, not a compliance requirement.

Is ensure_ascii=False output actually smaller?

In our test with café/Tokyo's Japanese name, yes — the escaped default was 45 UTF-8 bytes versus 35 for the unescaped form, since each \\uXXXX escape is 6 ASCII bytes while the real UTF-8 character is usually 2-4 bytes; the size gap grows with more non-ASCII content.

When should I keep the default ensure_ascii=True?

We'd keep it only when there's a specific reason to guarantee pure-ASCII output — for example embedding the JSON string inside another format or transport that can't safely carry raw UTF-8 bytes, or sending to a downstream system with unreliable encoding handling.

Does ensure_ascii affect the actual data, not just how it looks?

No — we confirmed it only changes the text representation on the wire or on disk; decoding either form with json.loads() produces identical Python objects, so switching it never breaks interoperability with a compliant parser.

Written with AI assistance. Measured on 2026-10-03 with Python 3.13.5's built-in json module, dumping the same Unicode text with and without ensure_ascii and comparing the exact output and byte length. PlainHeaders is our extension; Python's json module is built in.