Before you merge several json.dumps() call sites into one helper, record the exact bytes each site emits today and the error each one raises on bad input. Two call sites can load back into equal Python objects and still send different text over the wire. A byte-level pin catches that difference; a comparison of parsed dictionaries does not.
Why parsed equality is not enough
When a Python module is refactored, tests usually call json.loads() on the output and compare dictionaries. That check answers one question: do the decoded values match? It does not answer whether the text changed. Key order, whitespace, the escaping of non-ASCII characters, and the way NaN is written can all change while the decoded object stays identical.
As an Amazon Associate I earn from qualifying purchases.
The problem appears most often in older modules where each function passes its own keyword arguments to json.dumps(). One function may sort keys and use compact separators, another may keep the defaults, and a third may escape every non-ASCII character. A single helper with default settings would quietly merge those dialects. Any client that hashes, signs, diffs, or stores the raw body would then see a change even though no test failed.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →The approach described here comes from Dakota Huang’s DEV Community post dated “Sep 16” (the page shows no year). The steps are that author’s recommendations, not independently tested findings, and they are most useful when your code sends or stores serialized bytes.
#1 Best Overall
Step 1: Inventory call sites and their keyword arguments
Before you change any code, list every place that calls json.dumps() and record the keyword arguments each one passes. A search from the repository root is a practical start:
rg -n "json.dumps(" src/
For each hit, write down the kwargs, the payload type, and whether a default= handler is present. Group only sites whose settings are identical. Sites that differ in even one setting belong in separate groups, because they are separate dialects.
Step 2: Pin exact UTF-8 bytes and error behavior
For each dialect, choose a small representative payload and capture the output as bytes, not as a parsed object. Encode the string with UTF-8 and store it as a binary fixture. The pin should also record the exception type for any value that fails to serialize, plus whether a default= handler was in effect. A TypeError and a ValueError are different behaviors, and a refactor that changes one into the other is a real break.
Free tools Windows power users keep installed
One-click scans. No signup required.
The author’s sample harness uses a dataclass to describe each case, keeps the fixtures in a directory of binary files, and compares freshly generated bytes against them. The following sketch shows the shape. It is an illustration of the method, not a measured production run.
import json
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
@dataclass(frozen=True)
class Case:
name: str
payload: object
kwargs: dict
def handler(obj):
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, Decimal):
return str(obj)
raise TypeError(f"not serializable: {type(obj).__name__}")
def emit(case):
return json.dumps(case.payload, **case.kwargs).encode("utf-8")
CASES = [
Case("sorted_compact", {"b": 1, "a": "x"},
{"sort_keys": True, "separators": (",", ":")}),
Case("compact_non_ascii", {"name": "José"},
{"separators": (",", ":")}),
Case("spaced_decimal_datetime",
{"amount": Decimal("12.50"), "at": datetime.fromisoformat("2026-10-08T09:00:00+00:00")},
{"default": handler}),
]
Each case writes its bytes to tests/fixtures/json/<name>.bin. Inspect a fixture with a hex dump when you need to see exactly what is there:
xxd -l 64 tests/fixtures/json/compact_non_ascii.bin
Pin only what each call site actually uses. If a site never passes allow_nan, do not add it to the pin, because that adds a setting nobody depends on.
Rank #3
The keyword arguments that change output
The author lists six settings that can alter either the emitted text or the error raised. The table summarizes what each one changes and what to record when you pin it.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
| Setting | What it can change | What to record in the pin |
|---|---|---|
sort_keys |
Key order in the output. Dictionaries with the same items in a different insertion order produce different bytes when it is True. |
Whether it is set, and a payload whose keys are inserted out of order |
ensure_ascii |
Whether non-ASCII characters are written as uXXXX escapes (the default) or as UTF-8 characters. |
A payload containing at least one non-ASCII character |
separators |
The whitespace after commas and colons. The default is (", ", ": "); compact output uses (",", ":"). |
The exact tuple passed |
default |
The handler called for objects the encoder does not support. It determines whether a value serializes or raises. | Whether a handler exists, its identity, and the exception it raises for unknown types |
allow_nan |
With the default True, NaN and infinity values are written out. With False, they raise ValueError. |
The setting and the exception type for a non-finite float |
skipkeys |
With the default False, a key that is not a basic type raises TypeError. With True, that key is dropped silently. |
The setting and a payload with a non-basic key |
Three of these settings are the ones most often overlooked when code is consolidated. sort_keys and separators change bytes on valid input. default and skipkeys change which inputs fail, and how. allow_nan decides whether a non-finite float is a silent output or a loud error.
Step 3: Extract one kwargs group at a time
The author’s suggested sequence keeps each change small enough to verify. Follow it in this order:
- Commit the inventory and the binary fixtures on their own, before any extraction.
- Prove the pin can fail. Deliberately change one option in a throwaway branch, such as swapping
sort_keys, and confirm the byte comparison reports a difference. - Extract a helper for exactly one kwargs set. Keep the helper’s defaults identical to that group’s settings, not to the library’s defaults.
- Update only the call sites that belong to that group. Leave every other site alone.
- Inspect the diff. Every changed line should be a call-site swap, with no edits to settings or payloads.
- Rerun the pin suite. The fixtures must still match byte for byte.
The most important rule is at step 6. If a pin fails after extraction, the helper is wrong or the call site was moved to the wrong group. Do not regenerate the fixtures to make the failure disappear. A regenerated fixture records whatever the new code emits, which means it no longer tests anything.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Where byte pins fall short
A byte pin tests representation. It does not test whether the JSON describes the right data. The author states that byte pins do not establish schema correctness, and that drift in field names, types, or required keys needs a separate contract test. A pin and a contract test check different things, and one cannot replace the other.
The method also needs care in three situations:
- Streaming JSON lines with timestamps. Each line changes on every run unless the clock is controlled. Freeze the clock for any payload with a time field, or the pin will fail for reasons that have nothing to do with the refactor.
- Unordered set iteration. If a default handler converts a set by iterating over it, the output order can vary between runs. Sort the elements in the handler before serializing, so the bytes are deterministic.
- Standing in for an HTTP contract test. A fixture of a request body shows what your code wrote, not what the server accepts. Keep the HTTP-level test for that.
Also treat an intentional pretty-print change, such as adding indent=2, as a new dialect. Give it its own case and its own fixture rather than editing the existing pins.
When to skip the extraction
The author advises against this extraction in several conditions, and they are worth checking before you start:
- There is no byte-level test runner yet. Build the runner first.
- All call sites already share one kwargs dictionary, so there is nothing to merge.
- The module only emits debug logs, where the exact bytes are not observed by any client.
- Project policy forbids committing payload shapes to the repository.
The author also claims that output for the flags discussed is stable on current CPython. That is the author’s assertion, not a compatibility guarantee from the Python project. Rerun the pins whenever the interpreter changes, and run them on a second runtime if your deployment uses more than one.
As the author puts it, “Wire clients consume bytes, not Python dicts.” That sentence is the reason to pin bytes before you move any call site.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →The DEV Community post is the only source behind this article, so its recommendations reflect one author’s workflow. Check the behavior of each keyword argument against the Python standard library documentation for your version before you rely on it.
Quick Recap
“
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




