4

I wonder how to make this getter more type-safe:

VALUES = {
   '1': 'One',
   '2': 'Two',
   '3': 'Three'
}


def get(key : str) -> str:
    return VALUES[key]

Instead of the type str I would love to have a keyof VALUES and type(VALUES[key]) types.

get('4') should throw a invalid type warning then as this key does not exist. Not sure if this is possible with Python as I properly live in a TypeScript wonderland... :-)

TypeScript would properly look like this:

get<K extends keyof VALUES>(key : K): typeof K
{
    return VALUES[key];
}
Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
Henry Ruhs
  • 1,533
  • 1
  • 20
  • 32
  • 1
    `get('4')` already throws a `KeyError` as this key does not exist, I'm not sure what would be the difference to what you had in mind. – mkrieger1 Aug 10 '21 at 21:30
  • Consider making them class attributes instead. – o11c Aug 10 '21 at 21:30
  • @mkrieger1 they want it for static type checking, not a runtime error... – juanpa.arrivillaga Aug 10 '21 at 21:31
  • @o11c I agree that likely, you just want a different approach in Python. Python dict's are not supposed to be used like JS/TS objects. They are like JS *Map* objects. Unfortunately, their very similar literal syntax leads to equivocation – juanpa.arrivillaga Aug 10 '21 at 21:32
  • 6
    Possibly the proper solution would be to use an `Enum` instead of a dictionary. So that instead of having function parameters with type `str` you will have function parameters with type `VALUES` (and you don't need a `get` function). – mkrieger1 Aug 10 '21 at 21:36
  • Correct, I would like to have type checking via mypy and see false usage of not existing keys. Thanks anyway - you guys are awesome – Henry Ruhs Aug 10 '21 at 21:45

2 Answers2

5

You cannot do this in general. However, you can accomplish what you want in this particular case using typing.Literal:

import typing
def get(key: typing.Literal['1','2','3']) -> str:
    return VALUES[key]
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
  • 1
    Also, you can't check with plain Python if your code would type-check, but you can use a tool such as [mypy](http://mypy-lang.org/) to handle it for you. – enzo Aug 10 '21 at 21:29
  • 1
    @enzo of course, the OP is aware I'm pretty sure – juanpa.arrivillaga Aug 10 '21 at 21:30
  • Well, the keys are not just 1, 2 and 3... more complex in the final code - I played arround with VALUES.keys() and Literal but that doesn't quite fit together – Henry Ruhs Aug 10 '21 at 21:48
  • 1
    @redaxmedia you will *not* be able to do this dynamically (well, not if you want static analyzers to understand it), you will have to annotate it explicitly. – juanpa.arrivillaga Aug 10 '21 at 22:46
  • @redaxmedia: Not surprisingly, `typing.Literal` only works with [literals](https://docs.python.org/3/reference/expressions.html?highlight=literals#literals). – martineau Aug 10 '21 at 23:04
0

As was suggested in the comments, the enum module provides a nice solution here. By mixing in str with enum.Enum, we can create an Enum that is fully backwards-compatible with str (i.e., can be used wherever a str type is expected.

from enum import Enum

# The mixin type has to come first if you're combining
# enum.Enum with other types 
class Values(str, Enum):
    N1 = 'One'
    N2 = 'Two'
    N3 = 'Three'

If we enter this definition into the interactive console, we'll see that this class has the following behaviour:

>>> Values['N1']
<Values.N1: 'One'>
>>> Values('One')
<Values.N1: 'One'>
>>> Values.N1
<Values.N1: 'One'>
>>> Values('One') is Values.N1 is Values['N1']
True
>>> Values.N1.name
'N1'
>>> Values.N1.value
'One'
>>> Values.N1 == 'One'
True
>>> Values.N1 is 'One'
False
>>> Values.N1.startswith('On')
True
>>> type(Values.N1)
<enum 'Values'>
>>> for key in Values:
...     print(key)
...
Values.N1
Values.N2
Values.N3
>>> list(Values)
[Values.N1, Values.N2, Values.N3]

As you can see, we have defined a new type which:

  • Provides both dynamic dictionary-like access to members, as well as more type-safe forms of access.
  • Is fully backwards-compatible with str — it can freely be compared with str objects, and str methods can be used on its members.
  • Can be used in type hints as an alternative to typing.Literal. If I have a function like this:
def do_something(some_string):
    if some_string not in ('One', 'Two', 'Three'):
        raise Exception('NO.')

then I can either annotate it like this:

from typing import Literal

def do_something(some_string: Literal['One', 'Two', 'Three']) -> None:
   ...

or like this (in which case you'll have to pass in a member of the Values enum rather than a vanilla string, or the type-checker will raise an error):

# Values enum as defined above 
def do_something(some_string: Values) -> None:
   ...

There's an even more detailed guide to the intricacies of python Enums here.

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
Alex Waygood
  • 6,304
  • 3
  • 24
  • 46