-
Notifications
You must be signed in to change notification settings - Fork 241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce a Not type #801
Comments
In Kotlin the root type In TypeScript the root type Adding a |
I think I also have a use case for a I have a class, something like this: class Field(Generic[T]):
kind: FieldKind[T] # has many subclasses, like StringKind, ObjectKind[T], etc
def __init__(kind_spec: Union[FieldKind[T], Type[FieldKind[T]], Callable[..., T]]):
... The intent is that def field_kind_from(
spec: Union[FieldKind[T], Type[FieldKind[T]], Callable[..., T]]
) -> FieldKind[T]:
if isinstance(spec, FieldKind):
return spec
if isinstance(spec, FieldKindMeta):
return spec()
if isinstance(spec, SchemaMeta):
return ObjectKind[T](spec)
raise TypeError(f"{type(spec)} cannot be used as a FieldKind") ...so that I can write all 3 of these and have the type inference work: field_1 = Field(StringKind()) # Field[str], kind is StringKind()
field_2 = Field(StringKind) # Field[str], kind is StringKind()
field_3 = Field(SomeClass) # Field[SomeClass], kind is ObjectKind[SomeClass]() However, because Right now, |
I have yet another use case for class Mapping(Collection[_KT], Generic[_KT, _VT_co]):
@overload
def get(self, __key: Not[_KT]) -> None: ...
@overload
def get(self, __key: object) -> _VT_co | None: ...
@overload
def get(self, __key: Not[_KT], default: _T) -> _T: ...
@overload
def get(self, __key: object, default: _T) -> _VT_co | _T: ... Meaning: any key type that for certain cannot contain This means you don't have to narrow your key type before using |
My usecase is much less theory-driven - I just want to be able to safely chain functions using an
^ this has some nasty callback protocol for its type (https://mypy.readthedocs.io/en/stable/protocols.html#callback-protocols) but a |
I think I may have another real use case for Not (combined with Intersection) As described in python/mypy#13893, I want to use boolean literals to forbid a specific combination of function parameter types and boolean flag values. However, the required bool-type fallback for the boolean Literals, would catch and allow the forbidden combination, rendering the whole overload specialization useless. This is what doesn't work (full mypy example here)
If I had Not and Intersection, I could replace the latter fallback overload with:
|
Having a not type would allow using literals to define keys for a dictionary where those keys are required. Then having Not[the key literals] would allow one to define an additionalProperty key and corresonding value type which applies to all other keys. That would be useful in json schema contexts. |
On 22-23 July 2023 at the PyCon Europe in Prague I would like to discuss this Issue (together with others) and maybe formulate a PEP. |
We already have I argue that while |
I brought up the topic with the TypeScript team a while ago, and they said that they've never seriously considered adding such a concept to TypeScript because of all the problems it would create. I recommend focusing your attention on intersections and abandoning the notion of adding a |
I'm not as negative as Eric about this idea overall, but FWIW I agree that there's little use in pursuing it, really, until we have an intersection type. Its use cases are much more limited without an intersection type, and intersection types have a much broader array of use cases than a As Eric says, there will be enough challenges with intersection types that it makes sense to take the two separately. |
@erictraut can you elaborate on why
so defining what |
I'm saying that negated types will not compose with existing typing features. The feature would require many caveats and special cases limiting where it can be used. Also, depending on how generalized it is, it would be an enormous amount of work to support in type checkers. I'm aware that several people have supplied use cases to justify the introduction of type negation, but none of them (IMO) come close to being compelling enough to justify adding such a complex and unusual addition to the type system. Off the top of my head, here are some examples of where it won't compose, will introduce type holes, or will require special handling. # Type variables and constraint solving
def func0(x: Not[T]) -> T: ...
func1(0) # What is the type of this expression?
def func1(x: Not[T], y: T) -> T: ...
func2(0, 0) # What is the type of this expression? Should it be an error?
# Constrained type variables
X = TypeVar("X", str, Not[str]) # Is this allowed? What does it mean?
# Instantiable types
type[Not[int]] # What does this mean?
Not[type[int]] # Is it the same as this?
def func3(val: Not[Foo]):
type(val) # Is this valid? What is the type of this expression?
# Multiple inheritance
def func3(val: Foo):
x: Not[Bar] = val # Should this be allowed? What if val is an instance of a class that derives from both Foo and Bar?
# Structural subtyping
def func4(val: SupportsAbs):
x: Not[SupportsIndex] = val # Should this be allowed? What if val happens to conform to both protocols?
# Various other special cases
Not[object] # Is this equivalent to Never?
Not[Any] # What does this mean? Is it equivalent to Never?
Not[Never] # Is this the equivalent of object? Any? Some new top type that is not expressible today?
Not[Self] # Is this permitted?
# Recursive type aliases
X: TypeAlias = list[X] | Not[X] # This would need to be rejected with special-case logic because it introduces a circularity I'm sure I could come up with dozens more issues and questions if I spent a bit more time. This list is just the tip of the iceberg. And I'm focusing here only on the static type checking issues. The runtime issues would also need to be considered (e.g. including Not[X] in Many issues will go undiscovered unless/until someone takes the time to implement a reference implementation in one of the major type checkers. As with any new type feature, I recommend that prior to writing any draft PEP you familiarize yourself with other typed languages and if/how they implement the feature. For example, I did this with PEP 695 (see appendix A). I'm not aware of any other typed languages that implement this capability, and I suspect there's a good reason for that. You should ask yourself why Python is special in that it needs such a feature when other typed languages — even those that have much more advanced typing features — do not implement this. As I mentioned above, I think theres a reasonably strong justification for intersection types, and there is precedent for this feature in other typed languages. I recommend that you focus your attention on intersections and abandon the notion of negated types, at least for now. Intersections have their own long list of challenges that need to be worked out, and they will also require an enormous amount of work to add to type checkers if they are fully generalized. If history is any indication, it will take multiple years after an intersection PEP is drafted before the major type checkers support such a feature. And it will take years more to shake out all of the bugs for such a feature. Don't underestimate the work involved. |
Do these other languages have as many unusual object relationships on core types? The previously mentioned Regarding the examples, sure I get that many of them will require special casing. But how many of those examples have legitimate ambiguity? I could point out at least a few of those - the constrained For the other examples of mixing-and-matching of constructs in arbitrary ways, I'd also ask whether existing typing constructs required the same special casing upon their introduction. I don't have a good example here, but I'd pose the question whether currently you can in fact stick anything into a |
Thanks, Eric! These are all great points. I can suggest natural answers to most of your questions, but I agree I also think we need to spec how existing features work before attempting |
Another application for NOT-type I just encountered: It could be useful to "abuse" type-checkers as typo-checkers:
Over at However, sometimes we do want dynamical string, for example when applying schema save in config-files. With @overload
def astype(dtype: Literal["float32", "float64", "int32", "int64", ...]) -> Series: ...
@overload
def astype(dtype: str & ~LiteralString) -> Series: ... Currently, one has to give up either typo-checking or dynamical strings. |
Linking:
These may be useful if someone wants to write a PEP one day. |
Most of this would be abject nonsense to apply Not to. The below shows what's valid on static types and on gradual types, and it's a well-understood and studied part of type systems, even with gradual typing The below is from section4.1; Note that negation is not valid on gradual types. Most of these are just examples that show that Not needs to be restricted in its use, not that it can't be added or wouldn't be useful. There are very clear reasons why python would benefit from this currently, the most frequent, currently unsolvable correctly "gotcha" being the only way to solve this one currently is an overload with str -> Never and a runtime check for str to raise on, which incurs runtime costs instead of just A clear set of things that Not would be correct to apply to:
A clear set of things where Not would be nonsense if applied to
The open question would be
* (Edited a prior caveat to be significantly more clear) There are problems with this that can be shown with partial type erasure. class Sequence(Protocol[T]):
...
class SequenceMutationMethods(Protocol):
...
type MutableSequence[T] = Sequence[T] & SequenceMutationMethods
type NeverMutableSequence[T] = Sequence[T] & ~SequenceMutationMethods x: list = []
y: Sequence = x
z: NeverMutableSequence = y
x.append(1) These problems go away if total consistent use is checked by type checkers. ** Edit: Stricter TypeGuards PEP essentially provides a use case for this, but in just the case of type guards. Not is useful (arguably required if we add intersections) in python because python has a combination of structural and nominal subtyping, as well as just value-based handling in the type system (Literals...), and currently requires all 3 to express some types properly, while having undesirable overlap between them in some cases. There's existing research and proofs that show where it is and isn't sound to use in a gradually typed type system, so it's not so much a matter of not being able to determine what is and isn't valid or meaningful, but whether or not the effort of adding it provides enough value to justify the addition to the type system. |
There a large number of complicated examples in this thread, so here's a usecase that might be overly simplified. A function that returns the same type as the object put put in, unless you explicitly pass in
Currently gets flagged as an "Overloaded function signature overlap with incompatible return types" because T cannot be specified to NOT be NoneType. |
This is a continuation of this discussion on the Intersection issue.
I have a different use case for the
Not[...]
type, but it is related to the original discussion. My usecase is to be able to tell the type checker that two types can never have an intersection. The specific case is for the phantom-types library where there are two typesNonEmpty
andEmpty
.Currently there is no way to tell mypy that a variable can't both be
NonEmpty
andEmpty
, so this code passes without error:The output shows that mypy interprets
i
as an intersection betweentuple
,Empty
andNonEmpty
:Of course this code would error at runtime, but it would be preferable if this could be expressible so that type checkers can give an error here, similarly to how mypy gives errors for unreachable code.
A suggestion for how this could be expressed is to allow subclassing a
Not
type, so simplified the definitions of the two mentioned types would like this:There are also other types in the library that would benefit from this like
TZAware
andTZNaive
.Edit: To clarify, I think that this type would be valuable even without an
Intersection
type which is why I opened a separate issue. This is because (as showed in the example) there already implicit intersections in mypy. I don't know how the other type checkers treats this.The text was updated successfully, but these errors were encountered: