We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Precise Type Hints with Literals and Generics step 9 of 26
Describe Nested JSON with a Recursive Type Alias
Your service logs settings that arrive as decoded JSON. That means
dictionaries, lists, strings, numbers, booleans and None, nested to any
depth. For each setting you log how many levels it nests, and a flat view
of its dictionary keys. First you need a type for "any JSON value".
assert solve({"a": {"b": 1}, "c": 2}) == (2, {"a.b": 1, "c": 2})
assert solve({"a": [1, {"b": 2}]}) == (3, {"a": [1, {"b": 2}]})
A type alias that refers to itself
A type alias is a name for a type. The type statement creates one:
type Json = str | int | float | bool | None | list[Json] | dict[str, Json]
Json appears on its own right-hand side. So a list[Json] can hold more
lists and dictionaries, to any depth. You need no quotes around the inner
Json, because Python does not evaluate the right-hand side until code
reads it.
An alias is the same type under another name. Unlike NewType in the
previous exercise, type Count = int makes Count and int
interchangeable.
What to write
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]]: ...
Inputs are already decoded, contain no cycles, and use string keys. Do not
change the input. solve returns (json_depth(value), flatten_keys(value, sep)).
Depth follows lists and dictionaries.
- A scalar, including
None, has depth 0. - A list or dictionary has depth 1 plus the largest depth of its children. The children of a dictionary are its values, not its keys.
- An empty list or dictionary has depth 1.
assert json_depth(None) == 0
assert json_depth({}) == 1
assert json_depth([]) == 1
assert json_depth({"a": {"b": 1}}) == 2
assert json_depth({"a": [1, {"b": 2}]}) == 3
Flattening follows only nonempty dictionaries, in dictionary order.
- If the root is not a dictionary, return
{"": value}. - For each entry whose value is a nonempty dictionary, flatten that value,
and put
key + sepin front of each key it returns. - Every other value stays as it is, under its own key. Lists and empty dictionaries are leaves, even a list that holds dictionaries.
- An empty root dictionary returns
{}. A nested empty dictionary is a leaf and keeps its key.
assert flatten_keys({}) == {}
assert flatten_keys({"a": {}}) == {"a": {}}
assert flatten_keys({"a": [1, {"b": 2}]}) == {"a": [1, {"b": 2}]}
assert flatten_keys({"a": {"b": 1}}, "/") == {"a/b": 1}
Edge cases
sepcan be any string, including"".flatten_keysdoes not escape keys, so two paths can produce the same key, and the later value replaces the earlier one.{"a": {"b": 1}, "a.b": 2}flattens to{"a.b": 2}. With the two entries in the other order, it flattens to{"a.b": 1}.- You may return leaf lists and dictionaries as they are, without copying.
Narrow with the real classes
Narrow Json with isinstance(value, dict) and isinstance(value, list).
isinstance(value, Json) raises TypeError, because an alias is not a
class. Annotate the output dictionary as dict[str, Json]. You need no
cast or # type: ignore.
Going further
- The
typestatement is new in Python 3.12. It creates aTypeAliasTypeobject, andJson.__value__gives you the right-hand side. - Older code writes
Json: TypeAlias = ..., with the inner"Json"in quotes. Python 3.12 deprecatedTypeAliasin favor of thetypestatement. - Python's type alias documentation
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.