But to me, basically any cast() has a smell, and I wouldn't want to legitimize it by recommending that idiom.
I think I don't feel this as much as you do... a type guard is just a verbose conditional cast. Using cast has the advantage of not requiring runtime changes / requiring fairly minimal changes to type checkers, but if it's a concern maybe we could give it its own (hopefully less smelly) bikeshed, say assert_static_is_subtype.
Okay, let's just agree to disagree. Not everybody has to feel as bad about casts as I do, and not everybody has to feel the need to abstract type guards as conditional functions as much as I do.
If you don't want to define a two-line function, is that because you're writing short scripts? Or is it because you're worried about the runtime cost of the function call? (Then you should be worried about the cost of cast() also. :-)
My reluctance to define a function comes from a couple things:
- For examples given in the PEP draft, like `len(val) == 2` or `all(isinstance(x, str) for x in val)`, I'd always write those inline today. Having to move around my code to define a two line but clunky looking function feels less ergonomic than adding a cast or an annotation. Especially if the function just gets used once or twice.
- Runtime type checking has a lot of caveats. For my example of `not val or isinstance(val, str)`, picking a name like is_str_list_based_on_first_element to describe those caveats is clunkier than the actual code that does the check.
- I sometimes write short scripts!
For a short script I probably would inline the check as well. Then again for short scripts I rarely bother calling mypy. (Though now that I'm using VS Code that excuse is weaker. :-)
None of that is deal breaking, but since I didn't love TypeGuard, I thought it was worth bringing up cast as a lightweight construct that could be used to make code that is currently inexpressible in our type system expressible. In my mind, if cast supported redefinition like this today, the bar for the acceptance of a type guard like construct would be higher.
Hm, I don't see this as a feature of cast -- I see it as a feature of redefinition (which currently isn't well standardized). I would be really disappointed if redefinition was allowed when the RHS is a cast() but not when the RHS is some other expression. Somehow to me that breaks the abstraction that cast() is just an expression, and that would weigh heavily on me.
It's true that I accept some other things that have the form of an expression but are treated as special syntax, e.g. TypeVar() and NewType(). But for those that's the *only* way it can be used. A cast() is legal in any position where an expression is allowed, and that makes it hard for me to swallow that when combining two separate things that each have well-defined meanings (assignments and casts) the semantics of their combination cannot be derived from their combined semantics (in particular, you are proposing that `var = cast(T, ...)` be allowed in some cases where `var = <some expression that has type T>` would not be allowed.
I would much rather put an effort in better standardization of redefinition semantics (which would make your example work without special-casing cast() in that position).
... discussion of safe cast ...
I bungled this minor suggestion — my terminology and links were a mess :-) I meant to refer to https://github.com/erictraut/peps/blob/master/pep-9999.rst#enforcing-strict-narrowing
and some previous discussion on this thread. That is, should it be possible to further check that we only narrow to a subtype of the original type, e.g. `Tuple[int, ...]` to `Tuple[int, int]` as compared to `List[object]` to `List[str]` (or `int` to `str`). If this were desirable, it could be accomplished by use of a different cast function, perhaps called cast_narrower or downcast.
I thought the discussion ended with agreement that we should not enforce strict narrowing for type guards (because of invariant types). That doesn't mean there aren't use cases for downcasts, but they are different than the use cases for type guards.
Hm. In my experience typeguard functions often naturally occur (though perhaps not for the examples given in the PEP) and they often abstract away checks that should not be inlined. So perhaps the use cases are just different?
Yeah, the use cases are definitely complementary / do not preclude one another. Like you say, I think there's an unmet need for `assert isinstance(x, TypeForm)` and cast redefinition could provide a solution for that.
Let's start a separate thread for that so it doesn't distract us from the typeguard discussion further. (Or we can keep it in the tracker until such time as you feel it's ready to be turned into a PEP.)
That said, I'd like to know more about what use cases you envision for TypeGuard beyond code that currently tends to be inlined / maybe the PEP could benefit from a discussion of those. The TypedDict example in the current draft is well taken, though :-) (I'll note that you could still use cast redefinition in the appropriate scope, but it might feel annoying).
The main use case really is two-liners that you write over and over, not once or twice. It's annoying that you are *forced* to inline those just because otherwise the type checker won't narrow the type in the "if true" branch. I could think of cases where I'd even abstract a single isinstance() call into a function just because the type I'm checking for has a name that's long or confusing. But it's definitely something that becomes more important for larger code bases.