Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nice way to turn a dict into a TypedDict?

Note: since this answer keeps getting upvoted - while there are still use cases for TypedDict, I'd consider using a dataclass instead today.


I want to have a nice (`mypy --strict` and pythonic) way to turn an untyped `dict` (from `json.loads()`) into a `TypedDict`. My current approach looks like this:
class BackupData(TypedDict, total=False):
    archive_name: str
    archive_size: int
    transfer_size: int
    transfer_time: float
    error: str


def to_backup_data(data: Mapping[str, Any]) -> BackupData:
    result = BackupData()
    if 'archive_name' in data:
        result['archive_name'] = str(data['archive_name'])
    if 'archive_size' in data:
        result['archive_size'] = int(data['archive_size'])
    if 'transfer_size' in data:
        result['transfer_size'] = int(data['transfer_size'])
    if 'transfer_time' in data:
        result['transfer_time'] = int(data['transfer_time'])
    if 'error' in data:
        result['error'] = str(data['error'])
    return result

i.e I have a TypedDict with optional keys and want a TypedDict instance.

The code above is redundant and non-functional (in terms of functional programming) because I have to write names four times, types twice and result has to be mutable. Sadly TypedDict can't have methods otherwise I could write s.th. like

backup_data = BackupData.from(json.loads({...}))

Is there something I'm missing regarding TypeDict? Can this be written in a nice, non-redundant way?

like image 379
frans Avatar asked Jan 30 '26 11:01

frans


1 Answers

When you use a TypedDict, all information is stored in the __annotations__ field.

For your example:

BackupData.__annotations__

returns:

{'archive_name': <class 'str'>, 'archive_size': <class 'int'>, 'transfer_size': <class 'int'>, 'transfer_time': <class 'float'>, 'error': <class 'str'>}

Now we can use that dictionary to iterate over the data and use the values for type casting:

def to_backup_data(data: Mapping[str, Any]) -> BackupData:
    result = BackupData()
    for key, key_type in BackupData.__annotations__.items():
        if key not in data:
            raise ValueError(f"Key: {key} is not available in data.")
        result[key] = key_type(data[key])
    return result

Note that I throw an error when the data is not available, this can be changed at your discretion.

With the following test code:

data = dict(
        archive_name="my archive",
        archive_size="50",
        transfer_size="100",
        transfer_time="2.3",
        error=None,
)

for key, value in result.items():
    print(f"Key: {key.ljust(15)}, type: {str(type(value)).ljust(15)}, value: {value!r}")

The result will be:

Key: archive_name   , type: <class 'str'>  , value: 'my archive'
Key: archive_size   , type: <class 'int'>  , value: 50
Key: transfer_size  , type: <class 'int'>  , value: 100
Key: transfer_time  , type: <class 'float'>, value: 2.3
Key: error          , type: <class 'str'>  , value: 'None'
like image 71
Thymen Avatar answered Feb 01 '26 01:02

Thymen



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!