Static type checking is generally a good thing, but there are cases where it needs to be over-ridden. In Python, many of the cases where type checking gets in the way stem from the handling of (in)variance in generics. For instance, I often run into something like this:
def foo() -> list[int]:
return [1, 2, 3]
def bar() -> list[int | str]:
# This causes a type checking error, but it's always totally safe
return foo()
In these cases one is often forced to resort to a cast just to shut up the type checker. Unfortunately the current typing.cast() is indiscriminate; while it will happily cast list[int] into list[int | str] it would also let me pass a dict or any other class.
In a great many cases, and particularly in cases born of Pythonās invariance rules, it would be valuable to have a form of cast that only passes type checking if content conforming to the type of the input will always be conformant to the type of the output. In practice this generally means ātreat the output type as co-variant, recursivelyā.
This sort of ācompatibility castā would allow coders to avoid overly board casts and get a great deal of value from type checking, but would still quiet the type checker for known good code, which improves the checkerās signal to noise ration.
Iām sure that some people will want to point out that ātreat the output type as co-variant, recursivelyā does not guarantee compatibility, especially for generics that are contravariant on some of their types. While true, the result is no less safe than the current, indiscriminate cast() method and this could be addressed by careful choice of naming (e.g. covariant_cast()or similar).
Iād be interested to hear thoughts from people whoāve spent more time thinking about Python type checking than I have!
a way to say āIām not keeping a reference to this, so you can call it whatever you want.ā
A downside I see to this is that: this would be appropriate for many existing functions, so it would be a lot of churn and noise to add it everywhere where it could be useful.
There are some real concerns to if thatās always safe. Assuming that nothing retains a reference to the list with the previous type, it can be safe.
Iām of the belief that the more generalized type projections and variance model of kotlin is a better long term goal here, as it wouldnāt require typecheckers to start also understanding when a reference still exists or an ownership model, but there could be other benefits to having typecheckers be aware of when references still exist for other cases of mutable narrowing that are currently unsafe and uncaught.
If this is something to be added as a safer escape hatch, I definitely prefer something that could actually checked by the typechecker like the example given by @beauxq with a move special form rather than another way of spelling ājust trust meā to the type checker, as I think it would be a better long term goal to have more cases where we can spell our intent to a typechecker and have the intent be checked for consistency, rather than have places where we just need to opt-out of rigid behavior that typecheckers donāt understand anything beyond.
There are some real concerns to if thatās always safe. Assuming that nothing retains a reference to the list with the previous type, it can be safe.
Assuming nothing retains a reference to the source or nothing modifies the list in the target, but yes.
Iām of the belief that the more generalized type projections and variance model of kotlin is a better long term goal here, as it wouldnāt require typecheckers to start also understanding when a reference still exists or an ownership model, but there could be other benefits to having typecheckers be aware of when references still exist for other cases of mutable narrowing that are currently unsafe and uncaught.
Iāve not looked at this aspect of Kotlinās variance model, so I will take a look. Thanks for the pointer.
If this is something to be added as a safer escape hatch, I definitely prefer something that could actually checked by the typechecker like the example given by @beauxq with a move special form rather than another way of spelling ājust trust meā to the type checker.
Iām specifically not looking for a ājust trust meā. Iām looking for something that is going to be checked by the checker to ensure compatibility while setting aside variance issues. Given the nature of (a) when this is needed most and (b) why mutable generic containers are invariant, perhaps Move is a better way to spell what Iām after. I primarily want a type-checked way to quiet the correct code I gave above, without having to resort to the overhead of:
def bar() -> list[int | str]:
# This is safe and correctly checked, but makes a copy
return list(foo())
and without having to re-implement the copier for every possible type. Having some way to tell the type checker āIām giving this up, so check that itās compatible but donāt worry about varianceā would be helpful.
Is it really OK that foo created a list[int] but now you are trying to add a string to it? What if foo wasnāt returning its own new list, but a reference to an existing list?
x: list[int] = [1,2,3]
def foo() -> list[int]:
return x
def bar() -> list[int|str]:
return foo()
y = bar()
y.append("foo")
If you allow this code, now x contains a str value.
@chepner Yes, I fully appreciate that just as with any sort of cast, you can shoot yourself in the foot with it. What Iām looking for is a little bit of a safety catch on the gun. Consider the following code:
def food() -> list[str]:
return ["spam", "eggs"]
def foot() -> dict[Any, Any]:
return {"target": "mouth"}
lunch: list[int | str]
...
# This should be accepted by the type checker, since the input is compatible
lunch = checked_cast(list[int | str], food())
...
# This should raise an error during type checking
lunch = checked_cast(list[int | str], foot())
Your example shows how problems arise from keeping a reference to the thing that got cast, which is why @beauxq ās suggestion of having a way to indicate āIām not keeping a reference to thisā is a good way to describe one of the biggest use cases. You can abuse this sort of cast just like you can abuse typing.cast(), but a compatibility-checking cast will catch foot-in-mouth errors like the example above in a way that a blind cast never will.