pythonpython-typing

Python 3 type Dict with required and arbitrary keys


I'm trying to type a function that returns a dictionary with one required key, and some additional ones.

I've run into TypedDict, but it is too strict for my purpose. At the same time Dict is too lenient.

To give some examples with what I have in mind:

class Schema(PartiallyTypedDict):
    name: str
    year: int

a: Schema = {"name": "a", "year": 1}  # OK
b: Schema = {"name": "a", "year": 1, rating: 5}  # OK, "rating" key can be added
c: Schema = {"year": 1, rating: 5}  # NOT OK, "name" is missing

It would be great if there was also a way of forcing values of all of the optional/arbitrary key to be of a specific type. For example int in example above due to "rating" being one.

Does there exist such a type, or is there a way of creating such a type in python typing framework?


Solution

  • Since PEP 655 there is a solution for this problem. In Python 3.11+ there are typing.Required and typing.NotRequired.

    This means we have two ways to do this.

    typing.Required approach

    The typing.Required type qualifier is used to indicate that a variable declared in a TypedDict definition is a required key.

    This means we can use the totality flag to mark any keys as not required. And then use the typing.Required to mark explicitly keys as required.

    from typing import TypedDict, Required
    class Schema(TypedDict, total=False):
        name: Required[str]
        year: Required[int]
        rating: int
    

    typing.NotRequired approach

    the typing.NotRequired type qualifier is used to indicate that a variable declared in a TypedDict definition is a potentially-missing key

    This means we can use the totality flag to mark any keys as required and use typing.NotRequired to mark explicitly some keys as not required.

    from typing import TypedDict, NotRequired
    # total=True is the default value and therefore can be omitted.
    # class Schema(TypedDict, total=True): 
    class Schema(TypedDict):
        name: str
        year: int
        rating: NotRequired[int]
    

    Result

    Both approaches enforce dicts to have a name and a year key. It also enforces that, if a rating key is in the dict, the value is of type int.

    There is no restriction in adding further keys and there is no value-type restriction for those keys.

    This means:

    # OK
    a: Schema = {"name": "a", "year": 1}
    
    # OK, "rating" key can be added
    b: Schema = {"name": "a", "year": 1, rating: 5}  
    
    # NOT OK, "name" is missing
    c: Schema = {"year": 1, rating: 5}  
    
    # NOT OK, "rating" is no int
    d: Schema = {"name": "a", "year": 1, "rating": "invalid type"}  
    
    # OK, because any (undefined) key can be added, with any type
    e: Schema = {"name": "a", "year": 1, "rating": 5, "undefined-key": "undefined-value-type" } 
    

    Just for completeness:


    Addendum

    There should be soon a way, to give type restrictions to arbitrary keys. PEP 728 brings up the extra_items parameter for TypedDict.

    So it should be possible to do something like:

    from typing import TypedDict 
    class Schema(TypedDict(extra_items=int)):
        name: str
        year: int
    

    Which should result in:

    # Ok
    a: Schema = {"name": "a", "year": 1, rating: 5}
    
    # Ok
    b: Schema = {"name": "a", "year": 1, rating: 5, another_arbitrary_key: 4}
    
    # NOT Ok, because the arbitrary rating key is no int
    a: Schema = {"name": "a", "year": 1, rating: "five"}
    

    You may have expected the use of "should" this is, because at the time of writing this addendum the PEP 728 is not yet implemented.