'mypy allow redefinition at a specific line
I'm trying to lint my python code with mypy. A utility that I depend on uses a callback to which arguments can take multiple types. Type is typically defined by an accompanying topic string.
I'm having an issue in a situation where a callback takes multiple topics, each with a unique type. In this case, I'm unsure how to communicate to mypy
that type is more restricted than the union of the two.
def callback(self, topic: str, msg: Union[A, B]):
if topic == "a":
msg: A
# Do something specific to topic "a"
if topic == "b":
msg: B
# Do something specific to topic "b"
I've tried writing msg: A # type: ignore
instead, but then mypy just ignores this line. I also cannot use isinstance
due to the that utility's structure.
Potential solutions
There are two things that I believe should work, but I'm uncertain if mypy
has such capabilities:
- Allow redefinition on specific line. When running with
--allow-redefinition
the error is suppressed. Unfortuantely, running it for the whole package is too loose in my view, so I would prefer to avoid it. - Bind type of variable to value of another variable. I'm unsure if it exists, but it can be similar to
TypedDict
where keys can bind to different types of values, instead on two separate variables.
Solution 1:[1]
Overload signatures will make your function externally correct, even if you have to cast on the inside.
class Example:
@overload
def callback(self, topic: Literal["a"], msg: A) -> None: ...
@overload
def callback(self, topic: Literal["b"], msg: B) -> None: ...
def callback(self, topic: str, msg: Union[A, B]):
if topic == "a":
a_msg = cast(A, msg)
reveal_locals()
if topic == "b":
b_msg = cast(B, msg)
reveal_locals()
(Note: Literal
was added to typing
in Python 3.8. If you're using an earlier version, you can get it from typing_extensions
)
Inside the function, we still have to cast explicitly, but externally, the caller will always be required to call the function with either (1) the literal "a" and an A
, or (2) the literal "b" and a B
.
Solution 2:[2]
It takes some annoying boilerplate and a bit of a hack, but you can do this with a PEP 647 TypeGuard.
TypeGuards exist to tell MyPy that some custom function is able to do type narrowing in a similar way to things like isinstance. For example, you might have a function which takes a list and returns True if everything in that list is an integer:
def are_all_ints(probe: List[Any]) -> TypeGuard[List[int]]:
return all(isinstance(int, p) for p in probe)
xs: List[Any] = ...
if are_all_ints(xs):
# MyPy trusts the check was done correctly and therefore
reveal_type(xs) # this gives List[int] within this section
As you can see, TypeGuards operate on the variable that is passed to them. You could get narrowing of your topic
string if you pass it to a suitable typeguard function, but not of msg
. The workaround for that is to package them together into a temporary variable.
def callback(self, topic: str, msg: Union[A, B]):
def is_callback_from_a(p: Tuple[str, Union[A, B]]) -> TypeGuard[Tuple[Literal["a"], A]]:
return p[0] == "a"
def is_callback_from_b(p: Tuple[str, Union[A, B]]) -> TypeGuard[Tuple[Literal["b"], B]]:
return p[0] == "b"
# Package the two parameters into one variable so that
# the guard clears them simulataneously
topic_msg_pair = (topic, msg)
if is_callback_from_a(topic_msg_pair):
...
# topic_msg_pair[1] is known to be an A here
elif is_callback_from_b(topic_msg_pair):
...
# topic_msg_pair[1] is known to be a B here
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Silvio Mayolo |
Solution 2 |