Stdlib json.load[s]() return type

The json.load and json.loads both return typing.Any but why? Shouldn’t they be dict[str, Any]?

Reference: typeshed/stdlib/json/__init__.pyi at main · python/typeshed · GitHub

>>> json.loads('1')
1
9 Likes

Don’t forget hooks and custom decoders:

>>> import json
>>> json.loads('{}', object_hook = lambda _: 42)
42
>>> class D(json.JSONDecoder):
...     def decode(self, s):
...         return 42
...
>>> json.loads('{}', cls = D)
42
3 Likes

Thank you! I knew I was missing something and that explains it.

That’s pretty solvable with overloads (passing cls or any of the hooks uses an overload which returns Any). Last I heard, the issue with the return type (and the input to dump) was due to recursion.

Without nesting, the type would be List[Any] | Dict[str, Any] | str | float | int | None | bool (also I think the input can contain tuples). With nesting, replace the Any with this type ad infinitum.

Recursive type aliases are fine. The reason we use Any is that the full recursive type is not ergonomic to use for most users.

I understand that some users may want to opt in to this stricter check, though. It might be worth considering ideas like Introduce `typing.STRICTER_STUBS` · Issue #1096 · python/typing · GitHub (typing.STRICTER_STUBS) to allow people to use knobs to go from the more type-safe to the more ergonomic behavior.

4 Likes

It’s too bad we don’t have syntax to bind type variables at function call time, yet. Otherwise we could make load() generic:

def load[R=Any](...) -> R: ...

JSON: TypeAlias = dict[str, JSON] | list[JSON] | str | float | bool

x = load[R=dict[str, Any]](...)  # I know it must be a dict.
y = load[R=object](...)  # I want strict checking.
z = load[R=JSON](...)  # I also want strict checking, but have a type alias.
s = load[R=SomeShape](...)  # I have a TypedDict and trust the input.
3 Likes

What difference does it make to do

data = load[R=X](...)

vs

data: X = load(...)

?

4 Likes

If we had type variable binding syntax (which we haven’t), we could define load() in such a way that it’s possible to specify the return type. I.e., currently load() returns Any, but this could be overriden to make load() return whatever you define it to return:

x = load(...)  # x is Any
y = load[R=object](...)  # y is object

That doesn’t really answer my question. It’s not like binding the type variable makes the function any safer than just assigning the return value to a variable of constrained type.
And that’s precisely what the return type of Any allows, since Any is assignable to anything.

I think your argument makes more sense if the default for R was the recursive JSON type alias you defined, not Any.

It’s set to Any, because otherwise you can’t do this:

from typing import Any

JSON = dict[str, JSON] | list[JSON] | str | int | float | None
data: JSON = {"a": 1, "b": 2, "c": 3}
dct: dict[str, Any] = data

Output: (mypy Playground):

main.py:5: error: Incompatible types in assignment (expression has type "JSON", variable has type "dict[str, Any]")  [assignment]

3 posts were split to a new topic: My solution to typing json.loads