Narrowing from TypeIs that is only composed of all, iterating, and isinstance is incorrect currently.
I agree that the implementation looks reasonable, and so it would be nice if a type checker warned you that your guard is almost certainly going to be unsound.
I still think it’s not clear this is showing that the type system is broken, since the implementation of the TypeIs
, while reasonable, is incorrect (for example with identical reasoning, I could try to narrow an empty sequence to Sequence[Never]
which is more obviously incorrect - the underlying issue is the same).
And it’s not clearly showing that TypeIs
is worse than TypeGuard
. In this example the bug is identical using TypeGuard
, so negation isn’t playing much of a role here, even the positive narrowing is not correct.
There’s also the linked post that nobody has addressed yet.
I’m am interested in the idea that Not
is unsound on a variety of types, and how we think the TypeIs
operator should behave in these cases.
But I’m not succeeding in figuring out exactly what the claim and evidence is.
I can see a linked paper that says Not
is invalid on gradual types, which makes sense to me (if we were to adopt the notion that Any
is basically an unbound type variable representing any one static type, it’s pretty clear you can’t take the complement of that).
I don’t see an argument explaining why it cannot be applied to static types. I do see a claim that it doesn’t work with an example
type MutableSequence[T] = Sequence[T] & SequenceMutationMethods
type NeverMutableSequence[T] = Sequence[T] & ~SequenceMutationMethods
x: list = []
y: Sequence = x
z: NeverMutableSequence = y
x.append(1)
but I’m not sure what this example is demonstrating. I am unclear why a type checker should accept that y
is a valid instance of NeverMutableSequence
, that seems like a type error to me because I need “evidence” in the type checker that y
is immutable, and I don’t have that (which is a good thing because it is very much mutable!)
As a result, I don’t see why this example is demonstrating a problem with Not
and structural types.
I’m not trying to argue here that Not
is okay for structural types, just communicating that I am not finding an argument I can follow about the issue.
I do think that the TypeIs
example shows that Not
might actually be tricky in the presence of generic types, although the problem seems more related to type narrowing than to Not
itself - in fact it must be an issue with narrowing rather than Not
, because the problem is exactly the same using TypeGuard
which has no negative case.
So I still don’t feel like I have a clear grasp of the implications here.
I’m still trying to follow also the claim that TypeIs
is supposed to be more type-safe than TypeGuard
. It is a narrowing tool with similar behavior, and it is actually more powerful which means it is strictly harder to implement correctly (any valid TypeIs
is a valid TypeGuard
but not vice-versa), I don’t think any existing docs contradict this.
From the PEP, I think this line is technically correct but a little problematic:
For a function returning TypeIs[T]
to be safe, it must return True
if and only if the argument is compatible with type T
, and False
otherwise. If this condition is not met, the type checker may infer incorrect types.
It is technically correct because the argument is a python value with live references, and meeting this requirement means your type guard needs to be able to guarantee that the type narrowing is valid across all references. If that’s provably impossible for the type you are narrowing, then your type guard is provably unsound (and this is true for both TypeGuard
and TypeIs
).
I do think it’s a little misleading because people tend to substitute that “the argument value could be consistent with the type” which is a different claim and is only usable when all data is immutable (so not very often in Python).
In the case of a covariant type where we are narrowing the type parameter, it is probably provably unsound in every scenario if soundness includes being sound across all possible subclasses, since I can always subclass with an invariant (as we do for Sequence
→ MutableSequence
).
@mikeshardmind
- TypeIs claims to be type safe in this case
I’m pretty sure this function does not meet the stated requirements to be sound for TypeIs
(or TypeGuard
for that matter). So I’m not seeing this.
I definitely agree it looks reasonable, but type narrowing is tricky and in fact this is just not valid.
- This same exact unsafe narrowing can’t be replicated without TypeIs
It’s true that the negative case gives the type checker even more room to do something weird here, but since the positive case is already unsound I still don’t understand why we think this is a TypeIs
-specific problem.
Sorry for the long post with lots of questions.
I know folks in the intersection examples group have thought a lot about the type system and you probably have insights I don’t yet
I’m trying to learn, and I’m having a hard time following.
It seems like there are very precise claims being made based on examples where it isn’t clear that the root cause of the issues we see aren’t something related but different (e.g. this example is broken without negation, and without any structural types, so it’s hard to see how it could be demonstrating that negation doesn’t work on structural types).
I’m not trying to say that the claims themselves are wrong, just that I’m having trouble seeing the argument and evidence for the claims. The problem may be partly that I’m not very smart or knowledgable about type theory, but I think my background is actually pretty typical of Python typing folks.