diff --git a/Lib/json/__init__.py b/Lib/json/__init__.py index 37a86831ff94838..00d48685b92ebc5 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( @@ -233,6 +233,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 8768b63a3f80417..ded10a187b7a535 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 811880a15c80208..5e1a2a89cf8b075 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 5bc03085e60a3d3..d6d59c46596ca68 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({}), '{}') 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 000000000000000..35fbbe9261e9503 --- /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.dumps` bypass +:class:`json.JSONEncoder` and call the internal encoding logic directly. +This reduces the time required to encode small JSON objects by about 50% +with non-default options.