How to get key type from pythons TypedDict

Viewed 573

if I have a TypedDict like:

class Td(TypedDict):
    bar: str
    foo: str

and want to have a function thats parameter needs to be a key of that TypedDict, like:

def hello_Td(key: str, td: Td):
    return 'Hello ' + td[key]

With above code, I get a warning saying that key is str, expected Literal['bar', 'foo'].

Is there someway to automatically generate that literal-union type?

Something like:

td_keys = Literal(Td.keys())
def hello_Td(key: td_keys, td: Td):
    return 'Hello ' + td[key]

Update 1

RepEx: https://pastebin.com/vaVpJCBv enter image description here

1 Answers

Your first order of business should be to create a new data-type to represent the allowable dictionary keys.

Let us suppose that the new data-type is named Country

from enum import Enum, auto

class Country(Enum):
    def _generate_next_value_(name, start, count, last_values):
        return name
    RUSSIA        = auto()
    CHINA         = auto()
    UNITED_STATES = auto()
    AUSTRALIA     = auto()
    BRAZIL        = auto()

Let us inspect some examples:

example1 = Country("UNITED_STATES")
example2 = Country("RUSSIA")

print("str(example1)".ljust(20), " == ", str(example1))
print("repr(example1)".ljust(20), " == ", repr(example1))
print("example2".ljust(20), " == ", example2)
print("example1.value".ljust(20), " == ", example1.value)
print("example1.name".ljust(20),  " == ", example1.name)

The output printed to the console is as follows:

str(example1)         ==  Country.UNITED_STATES
repr(example1)        ==  <Country.UNITED_STATES: 'UNITED_STATES'>
example2              ==  Country.RUSSIA
example1.value        ==  UNITED_STATES
example1.name         ==  UNITED_STATES

Normally, enumerated data-types are whole numbers.
For example RUSSIA == 41.
However, it is more self-documenting to use strings.

Feel free to use something other than a enumerated data type (enum). The goal is simply to create a class to represent allowable dictionary keys.

Below is an implementation which is not an enumerated data-type:

import string

class Kountry:
    BRAZIL        = "BRAZIL"
    CHINA         = "CHINA"
    UNITED_STATES = "UNITED_STATES"

    def __init__(this, dirty_stryng:str):
        stryng = "".join(str(ch) for ch in dirty_stryng)
        stryng = stryng.upper()
        is_letter = lambda ch: ch in string.ascii_uppercase
        stryng = "".join(filter(is_letter, stryng))
        if not stryng in type(this).COUNTRIES:
            # print invalid input
            # maximum 30 characters. do not print 9,000 characters
            # remove line-breaks, carriage returns etc...
            # string representation of invalid input must fit all on one line
            msg = repr(str(dirty_stryng))[:30]
            raise ValueError(msg)
        this._string = stryng

    def __getattr__(this, attrname:str):
        if has_attr(this._string, attrname):
            return getattr(this._string, attrname)
        return getattr(type(this), attrname)

    def __str__(this):
        return this._string

We can take this non-enum out for a spin:

brazil = Kountry("brazil")
r = str(brazil)
print("str(brazil)".ljust(20), " is ", r)
r = brazil.split()
print("brazil.split()".ljust(20), " is ", r)
r = brazil.join("$-@")
print("brazil.join(\"$-@\")".ljust(20), " is ", r)

The console output is:

str(brazil)           is  BRAZIL
brazil.split()        is  ['BRAZIL']
brazil.join("$-@")    is  $BRAZIL-BRAZIL@

After defining a new data-type named "Country", you then want to do the following:

  1. Define functions whose input parameters must be instances of the Country class.

  2. Create a dictionary whose keys must be instances of the Country class.


We can create a function whose parameters are supposed to be instances of the Country class as follows:

def to_pretty_string(cntry:Country):
    country_str = str(cntry.name)
    out_str = "-xXx-".join(["", country_str, ""])
    return out_str

Where I wrote :Country on the input parameter... that is a python "type hint". The type-hint says that the function argument should be an instance of the Country class.

In case you are curious, we have the following output to the console when we call (use) the to_pretty_string function:

print(to_pretty_string(Country.BRAZIL))
print(to_pretty_string(Country.UNITED_STATES))
print(to_pretty_string(Country.RUSSIA))

# -xXx-BRAZIL-xXx-
# -xXx-UNITED_STATES-xXx-
# -xXx-RUSSIA-xXx-

In a language like C++, we would use a "template class" for the dictionary. In Java, they are called "generics" instead of "templates". However, it is the same concept whether it be called a "template class" or a "generic class"

I wish that the following was real code:

# WARNING: this is not real python

# Create a dictionary whose keys are Countries
head_counts = Dict[Country]

# Populate the dictionary
head_counts[Country.UNITED_STATES] = 329.5*1000000
head_counts[Country.RUSSIA]        = 144.1*1000000
head_counts[Country.BRAZIL]        = 212.6*1000000

# End of wishful thinking  

However, the python programming language does not have much in the way of generic classes in its standard libraries. You might have to write your own class in order to have a dictionary with string-only keys (Dict[str]) or a dictionary whose keys are decimal numbers only (Dict[float])

What we can do is create a type-hint.

However, the type-hint does not create a real dictionary; only a special object used in parameter lists to specify that the object should probably be a dictionary whose keys are Countries:

import typing as typ
import Country 

def funky_the_function(dee:typ.Dict[Country, object]):
    print("Keys to `dee` should be instances of `Country`")
    dee[Country.UNITED_STATES] = 329.5*1000000
    dee[Country.RUSSIA]        = 144.1*1000000
    dee[Country.BRAZIL]        = 212.6*1000000
    dee[342] # Danger Will Robinson! Danger!
    return
Related