The json.load and .loads in Python are customizable, allowing callbacks to be passed for transforming decoded JSON objects, key/value pairs, integers, floats, json constants - and notably absent is a callback to transform JSON arrays before the final Python object is returned.
With the introduction of frozendict, a combination of passing frozendict to either object_hook or object_pairs_hook, combined with passing tuple as an array_hook callback, is a straightforward way to get an arbitrarily nested imutable data structure.
The advantages of having a structure composed of mappings and sequences, but immutable will be the same that we get for having frozendicts in the first place. In particular, they provide data-structures which can make certain concurrent scenarios in free threading, or when sharing data across sub-interpreters easier to handle.
And, usefulness apart, this certainly addresses an asymmetry in the API, fixing the fact that from all possible JSON literals, just arrays are not directly customizable when decoding.
For sake of completeness, it is possible to replace the resulting lists from decoded JSON arrays in both object_hook and object_pairs_hook callbacks, but (1) that requires writing a full function with a for-loop looking for any lists, instead of, say, just passing a compatible Sequence class, and (2) if the JSON array happens to be the outermost container in the JSON string, this approach won't convert it, requiring some extra-checking in the caller code, after calling json.load.
Current approach:
frozen = json.loads(
json_str, object_hook=lambda dct: frozendict({k: (tuple(v) if isinstance(v, list) else v) for k, v in dct.items()}))
if isinstance(frozen, list):
frozen = tuple(frozen)
I'm well aware, a frozen=True option might make more sense for most people, but also makes things complicated because it would essentially conflict with object_hook.
Perhaps an even better solution would be to have a function specifically for decoding to a specific frozendict/tuple representation without the options so the complexity could be handled internally with a private API.
Activity
serhiy-storchaka commented on Mar 26, 2026
cc @etrepum
mpkocher commented on Mar 26, 2026
This seems like a hyper specific use case.
When would a user use anything other than tuple for array_hook=?
etrepum commented on Mar 26, 2026
I'm sure there are other hyper specific use cases when the schema of the JSON is well known, such as an array from the array module or numpy
mpkocher commented on Mar 26, 2026
Python typically aims for more for the 80/20 or 90/10 sort of model in the std lib, it doesn't really aim to solve hyper specific use cases.
etrepum commented on Mar 26, 2026
I'm well aware, a frozen=True option might make more sense for most people, but also makes things complicated because it would essentially conflict with object_hook.
Perhaps an even better solution would be to have a function specifically for decoding to a specific frozendict/tuple representation without the options so the complexity could be handled internally with a private API.
gh-146440: Add array_hook parameter to JSON decoders (#146441)
vstinner commented on Mar 27, 2026
Feature implemented by change d0061d5.
Example from the added tests:
$ ./python >>> import json >>> s = '{"xkd": [[1], [2], [3]]}' >>> data = json.loads(s, object_hook=frozendict, array_hook=tuple) >>> data frozendict({'xkd': ((1,), (2,), (3,))})pythongh-146440: Add array_hook parameter to JSON decoders (python#14…
pythongh-146440: Add array_hook parameter to JSON decoders (python#14…