So, most of the options various people have presented are not mutually exclusive options to improve the situation here. Some of them improve different aspects of the situation than others, so maybe we should compare what gets improved on them directly, what the costs are, and what the impact is for different interested parties (typecheckers and users, primarily)
Just changing the typeshed for str
If we just change the typeshed entry for str
to not inherit from Sequence[str]
, This âfixesâ the user perspective on Sequence[str]
, but not Iterable[str]
. It improves userâs expressibility, It improves soundness, has no complexity changes for type checker implementations, no new special casing is strictly required. It adds a cognitive complexity if anyone has a case where they rely on isinstance(x, Sequence)
, but that also subjectively removes a cognitive complexity of needing to evaluate every function that accepts Sequence[str]
, does it intend to accept str
Changing the typeshed for Sequence
We could instead change the typeshed for Sequence so that Sequence[T].__contains__
is only guaranteed to accept T
, then augment the various types that subclass it and accept object with more information. This would improve soundness, but have no other positive or negative impacts.
Adding a rule so that T is not assignable to G[T]
This would improve soundness and expressibility for users. It does not change soundness directly. It would increase complexity for typecheckers, but the rule generically applies and does not require special casing specific types. It solves both Iterable[str] and Sequence[str] cases presented alone. By the logic above, this likely improves cognitive complexity for users in some cases, but may harm it in others. This might warrant additional user diagnostic info or a specific callout for the str
case (The only one which can occur from composition of builtin types) in the typing docs.
Specifically, for SubscriptedType: ST
, and GenericType: GT
, if GT
is not exactly ST
, and ST
is a subtype of GT[ST]
, ST
is not assignable to GT[ST]
despite the subtyping relation, and requires writing GT[ST] | ST
.
Do nothing and wait for type negation
Type negation requires either positive proof at runtime or violating principles of gradual typing. It doesnât play well with covariant return types, which could have a wider type than expressed. Iâve been exploring this as part of all of: intersections, user denotable Not
, and an experimental non-specification compliant type-system designed around soundness. While it could help here, it would increase the number of runtime uses of isinstance
rather than decrease them, and would decrease ergonomics if done without violating principles of gradual typing over the current status quo (where the best option is check once, in the function that cares)
Change isinstance behavior to be aware of typing escape hatches
Improves soundness, disruptive to ABC use, increase in complexity for typecheckers. Likely no significant change to ergonomics.
typecheckers gain awareness of ABC.register
Same as above, but more limited in scope.
Add special type aliases
This could be used as a stopgap to make annotating intent easier, but this would exclude some valid cases. The current definition in useful_types
is fragile, and possible to break in a few ways, these include slightly less, but none of these allow str
itself.
type CommonSeqStr = list[str] | tuple[str]
type CommonIterableStr = Generator[str, Any, Any] | dict[str, Any] | set[str] | CommonSeqStr
Personal Assesment
A combination of Adding a rule with T not assignable to G[T], and modifying Sequence
in the typeshed (not modying str
in the typeshed!) so that str can be compatible with its definition would improve both soundness and expressibility with minimal churn for users. It would come at a cost of increased complexity for typecheckers and would likely require buy-in from existing typecheckers to accomplish. The other steps here would be possible to entertain separately from these to continue improving other aspects beyond the initial handling of what users currently feel is missing.
I believe this would also be the least disruptive option. Altering when typecheckers should narrow in isinstance would be the logical followup, but is significantly more complex and likely to have mismatches with user expectations, and I think requires a lot more care in evaluating.