We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Type System as a Design Tool step 9 of 24
A recursive Json alias, depth and flatten
Before Python 3.12, writing down the type of “some JSON” was genuinely painful.
You needed a quoted forward reference, and mypy’s support for the resulting
recursive alias was young and full of edges. Most codebases gave up and wrote
dict[str, Any], which is how a “fully typed” config loader ends up checking
nothing.
PEP 695’s type statement makes it a one-liner:
type Json = str | int | float | bool | None | list[Json] | dict[str, Json]
No quotes. The right-hand side is lazily evaluated — it is not executed
until something asks for Json.__value__ — so it may refer to itself, and to
names defined later in the module. The statement creates a TypeAliasType
object, which is generic-capable (type Pair[T] = tuple[T, T]) and recursive.
Three things worth pinning down:
-
X: TypeAlias = ...is deprecated as of 3.12. It still works; it is the transitional spelling from PEP 613. -
A bare assignment is ambiguous.
Json = str | intmight be a type alias or might be a variable holding a type object, and mypy’s guess produces the famously unhelpful “Variable … is not valid as a type”. -
type UserId = intis not a distinct type. It is a second name forint, andUserIdandintare interchangeable in both directions. If you wanted distinctness, that isNewType‘s job. Also:TypeAliasTypeis not a class, soisinstance(x, Json)is aTypeError.
The task
type Json = str | int | float | bool | None | list[Json] | dict[str, Json]
def json_depth(value: Json) -> int: ...
def flatten_keys(value: Json, sep: str = ".") -> dict[str, Json]: ...
def solve(value: Json, sep: str = ".") -> tuple[int, dict[str, Json]]: ...
json_depth — nesting depth. A scalar (including None) is 0. Any list
or dict is 1 + the maximum depth of its children, and 1 when it has none.
So {} and [] are both 1, and {"a": {"b": 1}} is 2.
flatten_keys — dotted-path flattening, dicts only:
-
If
valueis not a dict, return{"": value}. -
Otherwise, for each key: if the child is a non-empty dict, recurse and
prefix each of its paths with
key + sep; otherwise the child is a leaf and is emitted underkeyunchanged. -
Lists are leaves.
{"a": [1, {"b": 2}]}flattens to{"a": [1, {"b": 2}]}— the list is not descended into, even though it contains a dict. -
An empty dict is a leaf too, so
{"a": {}}flattens to{"a": {}}rather than vanishing.
solve returns (json_depth(value), flatten_keys(value, sep)).
Types
The recursive alias is doing real work. isinstance(value, dict) narrows Json
to dict[str, Json], so value.items() gives you str keys and Json values
with no further help; isinstance(value, list) narrows to list[Json]. Every
branch of both functions stays fully typed all the way down.
No Any, no cast, no # type: ignore. If you find yourself wanting one,
you have probably annotated the accumulator as a plain dict — which
--disallow-any-generics rejects anyway — instead of dict[str, Json].
Note what you cannot do: isinstance(value, Json) does not work, because
Json is a TypeAliasType and not a class. Narrowing goes through the concrete
runtime types the alias is built from.
Watch the depth definition
Depth counts containers, not keys. {"a": null, "b": {"c": 1, "d": "s"}} is
depth 2, not 3: the outer dict is one level, {"c": ..., "d": ...} is the
second, and the scalars inside contribute nothing. Getting this off by one is
the most common failure on this problem.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.