Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to cast a typing.Union to one of its subtypes in Python?

I’m using Python 3.6.1, mypy, and the typing module. I created two custom types, Foo and Bar, and then used them in a dict I return from a function. The dict is described as mapping str to a Union of Foo and Bar. Then I want to use values from this dict in a function that names only one argument each:

from typing import Dict, Union, NewType

Foo = NewType("Foo", str)
Bar = NewType("Bar", int)

def get_data() -> Dict[str, Union[Foo, Bar]]:
    return {"foo": Foo("one"), "bar": Bar(2)}

def process(foo_value: Foo, bar_value: Bar) -> None:
    pass

d = get_data()

I tried using the values as-is:

process(d["foo"], d["bar"])
# typing-union.py:15: error: Argument 1 to "process" has incompatible type "Union[Foo, Bar]"; expected "Foo"
# typing-union.py:15: error: Argument 2 to "process" has incompatible type "Union[Foo, Bar]"; expected "Bar"

Or using the types:

process(Foo(d["foo"]), Bar(d["bar"]))
# typing-union.py:20: error: Argument 1 to "Foo" has incompatible type "Union[Foo, Bar]"; expected "str"
# typing-union.py:20: error: Argument 1 to "Bar" has incompatible type "Union[Foo, Bar]"; expected "int"

How do I cast the Union to one of its subtypes?

like image 607
Chris Warrick Avatar asked Jun 24 '17 18:06

Chris Warrick


1 Answers

You'd have to use cast():

process(cast(Foo, d["foo"]), cast(Bar, d["bar"]))

From the Casts section of PEP 484:

Occasionally the type checker may need a different kind of hint: the programmer may know that an expression is of a more constrained type than a type checker may be able to infer.

There is no way to spell what specific types of value go with what specific value of a dictionary key. You may want to consider returning a named tuple instead, which can be typed per key:

from typing import Dict, Union, NewType, NamedTuple

Foo = NewType("Foo", str)
Bar = NewType("Bar", int)

class FooBarData(NamedTuple):
    foo: Foo
    bar: Bar

def get_data() -> FooBarData:
    return FooBarData(foo=Foo("one"), bar=Bar(2))

Now the type hinter knows exactly what each attribute type is:

d = get_data()
process(d.foo, d.bar)

Or you could use a dataclass:

from dataclasses import dataclass

@dataclass
class FooBarData:
    foo: Foo
    bar: Bar

which makes it easier to add optional attributes as well as control other behaviour (such as equality testing or ordering).

I prefer either over typing.TypedDict, which is more meant to be used with legacy codebases and (JSON) serialisations.

like image 118
Martijn Pieters Avatar answered Oct 02 '22 14:10

Martijn Pieters