Search code examples
pythonmypypython-typing

Python typing: Does TypedDict allow additional / extra keys?


Does typing.TypedDict allow extra keys? Does a value pass the typechecker, if it has keys which are not present on the definition of the TypedDict?


Solution

  • It depends.

    PEP-589, the specification of TypedDict, explicitely forbids extra keys:

    Extra keys included in TypedDict object construction should also be caught. In this example, the director key is not defined in Movie and is expected to generate an error from a type checker:

    m: Movie = dict(
          name='Alien',
          year=1979,
          director='Ridley Scott')  # error: Unexpected key 'director'
    

    [emphasis by me]

    The typecheckers mypy, pyre, and pyright implement this according to the specification.

    However, it is possible that a value with extra keys is accepted. This is because subtyping of TypedDicts is allowed, and the subtype might implement the extra key. PEP-589 only forbids extra keys in object construction, i.e. in literal assignment. As any value that complies with a subtype is always deemed to comply with the parent type and can be upcasted from the subtype to the parent type, an extra key can be introduced through a subtype:

    from typing import TypedDict
    
    class Movie(TypedDict):
        name: str
        year: int
    
    class MovieWithDirector(Movie):
        director: str
    
    
    # This is illegal:
    movie: Movie = {
        'name': 'Ash is purest white', 
        'year': 2018, 
        'director': 'Jia Zhangke',
    }    
    
    # This is legal:
    movie_with_director: MovieWithDirector = {
        'name': 'Ash is purest white', 
        'year': 2018, 
        'director': 'Jia Zhangke',
    }
    
    # This is legal, MovieWithDirector is a subtype of Movie
    movie: Movie = movie_with_director  
    

    In the example above, we see that the same value can sometimes be considered complying with Movie by the typing system, and sometimes not.

    As a consequence of subtyping, typing a parameter as a certain TypedDict is not a safeguard against extra keys, because they could have been introduced through a subtype.

    If your code is sensitive with regard to the presence of extra keys (for instance, if it makes use of param.keys(), param.values() or len(param) on the TypedDict parameter param), this could lead to problems when extra keys are present. A solution to this problem is to either handle the exceptional case that extra keys are actually present on the parameter or to make your code insensitive against extra keys.

    If you want to test that your code is robust against extra keys, you cannot simply add a key in the test value:

    def some_movie_function(movie: Movie):
        # ...
    
    def test_some_movie_function():
        # this will not be accepted by the type checker:
        result = some_movie_function({
            'name': 'Ash is purest white', 
            'year': 2018, 
            'director': 'Jia Zhangke',
            'genre': 'drama',
        })    
    

    Workarounds are to either make the type checkers ignore the line or to create a subtype for your test, introducing the extra keys only for your test:

    class ExtendedMovie(Movie):
         director: str
         genre: str
    
    
    def test_some_movie_function():
        extended_movie: ExtendedMovie = {
            'name': 'Ash is purest white', 
            'year': 2018, 
            'director': 'Jia Zhangke',
            'genre': 'drama',
        }
    
        result = some_movie_function(test_some_movie_function)
        # run assertions against result