unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
algorithmic-optimization

gh-150820: Speed up json.dumps() for small documents

Project: python/cpython | Language: Python | Concepts: algorithmic-optimization | Difficulty: mid
Link: https://github.com/python/cpython/pull/150827

The problem

The json.dumps() function in Python always creates a helper closure for formatting floats, even when the fast C encoder path is used and never actually calls this closure. For small JSON documents (which are common in real-world usage), this unnecessary closure creation adds measurable overhead.

Why it's non-trivial

A naive fix might simply remove or always create the float formatting helper, but that risks breaking the pure-Python fallback or introducing subtle bugs. The solution needs to avoid the closure allocation on the fast path without changing behavior or output, and without breaking compatibility with the slower, pure-Python path.

The approach

The core change is to only create the floatstr helper function when the pure-Python encoder is used, skipping its creation when the C encoder is available. This eliminates the per-call closure allocation on the fast path.

Original code (always creates the closure):

-        def floatstr(o, allow_nan=self.allow_nan,
-                _repr=float.__repr__, _inf=INFINITY, _neginf=-INFINITY):
-            # Check for specials.  Note that this type of test is processor
-            # and/or platform-specific, so do tests which don't depend on the
-            # internals.
-
-            if o != o:
-                text = 'NaN'
-            elif o == _inf:
-                text = 'Infinity'
-            elif o == _neginf:
-                text = '-Infinity'
-            else:
-                return _repr(o)
-
-            if not allow_nan:
-                raise ValueError(
-                    "Out of range float values are not JSON compliant: " +
-                    repr(o))
-
-            return text

This function is defined unconditionally, even though the C encoder (the common case) never uses it.

Patched code (creates the closure only if needed):

+        if self.indent is None or isinstance(self.indent, str):
+            indent = self.indent
+        else:
+            indent = None
+        if (_one_shot and c_make_encoder is not None and self.default is None):
+            return c_make_encoder(
+                markers, self.default, _encoder, indent,
+                self.key_separator, self.item_separator, self.sort_keys,
+                self.skipkeys, self.allow_nan)
+        else:
+            def floatstr(o, allow_nan=self.allow_nan,
+                    _repr=float.__repr__, _inf=INFINITY, _neginf=-INFINITY):
+                # Check for specials.  Note that this type of test is processor
+                # and/or platform-specific, so do tests which don't depend on
+                # the internals.
+
+                if o != o:
+                    text = 'NaN'
+                elif o == _inf:
+                    text = 'Infinity'
+                elif o == _neginf:
+                    text = '-Infinity'
+                else:
+                    return _repr(o)
+
+                if not allow_nan:
+                    raise ValueError(
+                        "Out of range float values are not JSON compliant: " +
+                        repr(o))
+
+                return text
+
+            _iterencode = _make_iterencode(
+                markers, self.default, _encoder, indent, floatstr,
+                self.key_separator, self.item_separator, self.sort_keys,

Now, floatstr is only defined and passed to the encoder if the C fast path is not taken. This avoids creating an unused closure on the fast path, improving performance for small documents.

NEWS entry trimmed for clarity:

+Speed up :func:`json.dumps` for small documents. Patch by Bernát Gábor.

The user-facing changelog is concise and omits technical details not relevant to end users.

From the review thread

eendebakpt: +1 on the change to floatstr (this is also part of my branch with a C version of the encoder https://github.com/python/cpython/compare/main...eendebakpt:cpython:json_floatstr, in my benchmarks this makes a difference)

gaborbernat: Thanks for the review and the split suggestion. Applied both: dropped the comment and trimmed the NEWS entry to Speed up :func:json.dumps for small documents.

Takeaways

  • Avoid allocating resources (like closures or objects) on hot code paths unless they are actually needed.
  • Micro-optimizations in widely used standard library functions can have a measurable impact, especially for common small inputs.
  • When optimizing, ensure that changes preserve compatibility and behavior for all code paths, not just the fast path.