I saw someone use typing.cast() the other day and it was my first encounter with it. At first, I thought, wow, maybe there is a way to re-type variables when they are reassigned. I was just looking at mypy when I should have been lookng at typing. But after I looked closer, I realized that typing.cast is actually meant to be used as an escape hatch when you have a very messy typing situation to resolve rather than to change the contract a variable has to obey.
This is likely the opposite of what most people need. Here's some code to demonstrate::
from typing import cast, Union
def multiply(a: int, b: int) -> int:
return a * b
number: Union[str, int] = input('Enter an integer number:')
try:
number = int(number)
except Exception:
pass
number = cast(int, number)
answer = multiply(number, number)
print(answer)
This code is intended to take some input from the user, transform it into and int, and then multiply it with itself. Simple! But of course, there's a bug in it, one that we want type checking to help us avoid. In pythonic fashion, we're using the same variable to represent the number when it is in string form, having just been entered by the user, and when it is in int form, having been transformed by our code. So we start off by defining the variable as being either str or int; the type that we initially receive and the type that we eventually want. This lets us get the value from the user and to store the transformed value into the same variable so it seems okay at first.
Later, whn we send it to the multiply function, type checking tells us that we can only send in
ints. Something that might be an int or might be a str is not welcome. So we realize we need
to re-type the variable to an int. No problem, we think, I just need to transform the number to an
int. I run cast(int, number)
and it should then work, right? The type checker has stopped
complaining. And when I run the program and type in "5" I get 25 printed to the screen. Hurray!
But as you can probably see, there is a glaring problem... There's two code paths and I'm only
transforming the str to an int in one of them. I'm only transforming it when the user's input
can be transformed into an int via the int()
function. What happens when someone enters
"gobbledy gook" instead of a number? Well, when executing the code, we see that number is
the str, "gobbledy gook", it is passed to int(), int will raise an exception, and then that
exception will be ignored [This is where our bug is]. When we cast "gobbledy gook" to int, neither
python nor the type checker complain. Instead, they just says, "you're the boss, boss" and relabel
the value as an int. Then, when we call multiply(number, number) python will throw an error because
the function expects ints but it's getting the string "gobbledy gook" instead. But the type
checker? The type checker doesn't catch it because as far as it's concerned, the value of number is
an int because we told it so.
The bug in our attempts to type check this code is that we mistook coercing a value to a new value
(cast(int, number)
) for changing the contract of what type a variable should hold. When we update
the contract, we are saying, previously, this variable could refer to a value of type int or str.
But from this point forward, this variable can only refer to a value of type int. Although mypy
can't do this, in pyre you can:
def multiply(a: int, b: int) -> int:
return a * b
# You don't have to use a Union here because you can just change the contract later
number: str = input('Enter an integer number:')
try:
number: int = int(number)
except Exception:
pass
# Nope! don't use cast unless you need it to escape from code which isn't typed correctly
# number = cast(int, number)
answer = multiply(number, number)
print(answer)
And now with pyre, you'll get a useful error:
Defaulting to non-incremental check.
Setting up a `.pyre_configuration` with `pyre init` may reduce overhead.
ƛ Found 1 type error!
test/type-checking.py:14:18 Incompatible parameter type [6]: Expected `int` for 1st positional only parameter to call `multiply` but got `typing.Union[int, str]`.
A brief tangent: Why is pyre saying that number has a Union type? We didn't specify that in our code. The reason is that there are two code branches that the code can descend. Either the happy path where number is transformed into an int and we change the contract or the error path where we don't update the contract and number is still a str. pyre can tell that we might go down either of these paths and therefore it knows that once the code path reunites after the exception handler, the variable can be either a str or an int. It therefore updates the contract automatically to say that number's contract to Union[str, int].