From d4c9f0c47049128ab486b66a5133a8678c4e6882 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 23 Sep 2026 14:51:04 +0000 Subject: [PATCH 1/4] Optimize JSON encoding with non-default options --- Lib/json/__init__.py | 15 ++++++++++++- Lib/json/encoder.py | 36 ++++++++++++++++++++++++++++++ Lib/test/test_json/test_default.py | 11 +++++++++ Lib/test/test_json/test_dump.py | 15 +++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/Lib/json/__init__.py b/Lib/json/__init__.py index 37a86831ff9483..05e91430fe1e23 100644 --- a/Lib/json/__init__.py +++ b/Lib/json/__init__.py @@ -103,7 +103,7 @@ __author__ = 'Bob Ippolito ' from .decoder import JSONDecoder, JSONDecodeError -from .encoder import JSONEncoder +from .encoder import JSONEncoder, _iterencode_oneshot import codecs _default_encoder = JSONEncoder( @@ -168,6 +168,12 @@ def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, cls is None and indent is None and separators is None and default is None and not sort_keys and not kw): iterable = _default_encoder.iterencode(obj) + elif cls is None and not kw: + iterable = _iterencode_oneshot( + obj, skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, + indent=indent, separators=separators, default=default, + sort_keys=sort_keys) else: if cls is None: cls = JSONEncoder @@ -233,6 +239,13 @@ def dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, default is None and not sort_keys and not kw): return _default_encoder.encode(obj) if cls is None: + if not kw: + it = _iterencode_oneshot( + obj, skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, + indent=indent, separators=separators, default=default, + sort_keys=sort_keys) + return ''.join(it) cls = JSONEncoder return cls( skipkeys=skipkeys, ensure_ascii=ensure_ascii, diff --git a/Lib/json/encoder.py b/Lib/json/encoder.py index 8768b63a3f8041..ded10a187b7a53 100644 --- a/Lib/json/encoder.py +++ b/Lib/json/encoder.py @@ -456,3 +456,39 @@ def _iterencode_once(o, _current_indent_level): del _iterencode, _iterencode_dict, _iterencode_list return _iterencode_once + + +# Same error as JSONEncoder.default(), without creating an encoder instance. +def _default(o): + raise TypeError(f'Object of type {o.__class__.__name__} ' + f'is not JSON serializable') + + +# Shortcut for JSONEncoder().iterencode(o, _one_shot=True). +def _iterencode_oneshot(obj, *, skipkeys=False, ensure_ascii=True, + check_circular=True, allow_nan=True, indent=None, + separators=None, default=None, sort_keys=False): + if c_make_encoder is None: + return JSONEncoder( + skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, + sort_keys=sort_keys, indent=indent, separators=separators, + default=default).iterencode(obj, _one_shot=True) + + item_separator = ', ' + key_separator = ': ' + if separators is not None: + item_separator, key_separator = separators + elif indent is not None: + item_separator = ',' + + if indent is not None and not isinstance(indent, str): + indent = ' ' * indent + + _iterencode = c_make_encoder( + {} if check_circular else None, + _default if default is None else default, + encode_basestring_ascii if ensure_ascii else encode_basestring, + indent, key_separator, item_separator, + sort_keys, skipkeys, allow_nan) + return _iterencode(obj, 0) diff --git a/Lib/test/test_json/test_default.py b/Lib/test/test_json/test_default.py index 811880a15c8020..5e1a2a89cf8b07 100644 --- a/Lib/test/test_json/test_default.py +++ b/Lib/test/test_json/test_default.py @@ -8,6 +8,17 @@ def test_default(self): self.dumps(type, default=repr), self.dumps(repr(type))) + def test_falsey_callable_default(self): + class Default: + def __bool__(self): + return False + + def __call__(self, obj): + return str(obj) + + self.assertEqual(self.dumps(type, default=Default()), + self.dumps(str(type))) + def test_bad_default(self): def default(obj): if obj is NotImplemented: diff --git a/Lib/test/test_json/test_dump.py b/Lib/test/test_json/test_dump.py index 5bc03085e60a3d..d6d59c46596ca6 100644 --- a/Lib/test/test_json/test_dump.py +++ b/Lib/test/test_json/test_dump.py @@ -9,6 +9,21 @@ def test_dump(self): self.json.dump({}, sio) self.assertEqual(sio.getvalue(), '{}') + def test_dump_options(self): + class Default: + def __bool__(self): + return False + + def __call__(self, obj): + return 'custom' + + sio = StringIO() + self.json.dump({'é': object(), 'a': [1, 2]}, sio, + ensure_ascii=False, indent=2, sort_keys=True, + default=Default()) + self.assertEqual(sio.getvalue(), + '{\n "a": [\n 1,\n 2\n ],\n "é": "custom"\n}') + def test_dumps(self): self.assertEqual(self.dumps({}), '{}') From 80a49a636db1756a409fb9eed9f2ace592fd2608 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 23 Sep 2026 14:59:28 +0000 Subject: [PATCH 2/4] remove default encoder --- Lib/json/__init__.py | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/Lib/json/__init__.py b/Lib/json/__init__.py index 05e91430fe1e23..d9c1cf5e7fc775 100644 --- a/Lib/json/__init__.py +++ b/Lib/json/__init__.py @@ -106,16 +106,6 @@ from .encoder import JSONEncoder, _iterencode_oneshot import codecs -_default_encoder = JSONEncoder( - skipkeys=False, - ensure_ascii=True, - check_circular=True, - allow_nan=True, - indent=None, - separators=None, - default=None, -) - def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw): @@ -162,13 +152,7 @@ def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, the ``cls`` kwarg; otherwise ``JSONEncoder`` is used. """ - # cached encoder - if (not skipkeys and ensure_ascii and - check_circular and allow_nan and - cls is None and indent is None and separators is None and - default is None and not sort_keys and not kw): - iterable = _default_encoder.iterencode(obj) - elif cls is None and not kw: + if cls is None and not kw: iterable = _iterencode_oneshot( obj, skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular, allow_nan=allow_nan, @@ -232,12 +216,6 @@ def dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, the ``cls`` kwarg; otherwise ``JSONEncoder`` is used. """ - # cached encoder - if (not skipkeys and ensure_ascii and - check_circular and allow_nan and - cls is None and indent is None and separators is None and - default is None and not sort_keys and not kw): - return _default_encoder.encode(obj) if cls is None: if not kw: it = _iterencode_oneshot( From cb3f01baf96b184f453a261fffcf5df871f5d479 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Sun, 27 Sep 2026 03:15:14 +0000 Subject: [PATCH 3/4] add news --- .../Library/2026-09-27-03-14-45.gh-issue-158270.ajnins.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-09-27-03-14-45.gh-issue-158270.ajnins.rst diff --git a/Misc/NEWS.d/next/Library/2026-09-27-03-14-45.gh-issue-158270.ajnins.rst b/Misc/NEWS.d/next/Library/2026-09-27-03-14-45.gh-issue-158270.ajnins.rst new file mode 100644 index 00000000000000..8a2a65723cc3a6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-27-03-14-45.gh-issue-158270.ajnins.rst @@ -0,0 +1,4 @@ +When possible, make :func:`json.dump` and :func:`json.dumps` bypass +:class:`json.JSONEncoder` and call the internal encoding logic directly. +This reduces the time required to encode small JSON objects by about 20% +with the default options and about 50% with non-default options. From e425c49243dcf70fff3c7008919562820b29fdba Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Sun, 27 Sep 2026 03:51:11 +0000 Subject: [PATCH 4/4] revert dump() optimization --- Lib/json/__init__.py | 28 +++++++++++++++---- ...-09-27-03-14-45.gh-issue-158270.ajnins.rst | 6 ++-- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/Lib/json/__init__.py b/Lib/json/__init__.py index d9c1cf5e7fc775..00d48685b92ebc 100644 --- a/Lib/json/__init__.py +++ b/Lib/json/__init__.py @@ -106,6 +106,16 @@ from .encoder import JSONEncoder, _iterencode_oneshot import codecs +_default_encoder = JSONEncoder( + skipkeys=False, + ensure_ascii=True, + check_circular=True, + allow_nan=True, + indent=None, + separators=None, + default=None, +) + def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw): @@ -152,12 +162,12 @@ def dump(obj, fp, *, skipkeys=False, ensure_ascii=True, check_circular=True, the ``cls`` kwarg; otherwise ``JSONEncoder`` is used. """ - if cls is None and not kw: - iterable = _iterencode_oneshot( - obj, skipkeys=skipkeys, ensure_ascii=ensure_ascii, - check_circular=check_circular, allow_nan=allow_nan, - indent=indent, separators=separators, default=default, - sort_keys=sort_keys) + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + default is None and not sort_keys and not kw): + iterable = _default_encoder.iterencode(obj) else: if cls is None: cls = JSONEncoder @@ -216,6 +226,12 @@ def dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, the ``cls`` kwarg; otherwise ``JSONEncoder`` is used. """ + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + default is None and not sort_keys and not kw): + return _default_encoder.encode(obj) if cls is None: if not kw: it = _iterencode_oneshot( diff --git a/Misc/NEWS.d/next/Library/2026-09-27-03-14-45.gh-issue-158270.ajnins.rst b/Misc/NEWS.d/next/Library/2026-09-27-03-14-45.gh-issue-158270.ajnins.rst index 8a2a65723cc3a6..35fbbe9261e950 100644 --- a/Misc/NEWS.d/next/Library/2026-09-27-03-14-45.gh-issue-158270.ajnins.rst +++ b/Misc/NEWS.d/next/Library/2026-09-27-03-14-45.gh-issue-158270.ajnins.rst @@ -1,4 +1,4 @@ -When possible, make :func:`json.dump` and :func:`json.dumps` bypass +When possible, make :func:`json.dumps` bypass :class:`json.JSONEncoder` and call the internal encoding logic directly. -This reduces the time required to encode small JSON objects by about 20% -with the default options and about 50% with non-default options. +This reduces the time required to encode small JSON objects by about 50% +with non-default options.