Allow zero arguments for subscript syntax

Sure, I am actually also in favor of that in the interested of increasing the readability and customizability of DSLs within python. But this is almost surely never going to happen.

Why on earth are you giving __slots__ a type annotation? Surely type checkers know that __slots__ is read-only and can therefore infer the type without an explicit annotation? And even if you want to, I fail to see what’s wrong with the existing way of writing it. What’s confusing to me about this code is why you’re annotating __slots__, and why you didn’t write it as Literal[()] rather than tuple[()], not the extra parentheses in tuple[()].

I don’t see this as a credible use case, I’m afraid.

1 Like

I give EVERYTHING a type annotation (unless the typing system doesn’t support it):

If you open this project with your type checker, there will be very little problems.

And Literal[()] is NOT a valid type annotation.

Why? What is the point?

I don’t have to think about when I need an annotation. And that way I’ll get the best suggestions of my IDE.

Sigh. This is why I get frustrated with the typing system (for reasons unrelated to the specific point we’re discussing here). What is the right way to say “the type that consists of just the literal value ()”?

Even if I say Literal[(),], it tells me “error: Parameter 1 of Literal[…] is invalid [valid-type]”. That’s mypy - pyright is more informative with “error: Type arguments for “Literal” must be None, a literal value (int, bool, str, or bytes), or an enum value”.

It looks like tuple[()] is the way to say this. Which is far more of a weird inconsistency than anything to do with needing a comma.

I’m going to duck out of this discussion now. The only thing I was trying to say is that changing subscription semantics to allow an empty subscript is far more disruptive than it’s worth. And this digression has made me feel even more strongly that this is the case - even if we did make that change, the other weirdnesses and inconsistencies in the typing system (like not being able to specify an empty tuple to Literal) would remain. My personal feeling is that Python typing is fine, as long as you use it sparingly and don’t try to push it too far. And this is definitely “too far”…

1 Like

tuple[()] without a doubt.

This is because no tuple can be used as an argument* for Literal - It would be inconsistent to special case the empty tuple.

* Ofcoure, tuples are used as the argument for Literal: Literal["a", "b"] can’t be distinguished from Literal[("a", "b")], which is again the same core problem.

2 Likes

There was a discussion a while ago about allowing empty subscript
expressions for another reason, can’t remember what now. Ultimately it
was rejected. You might want to look back at that.

I can’t take code golf as a strong reason this is something that needs to exist

I think you’re fundamentally misunderstanding the point of type annotations paired with static analysis, and Paul is right here.

type annotations, paired with static analysis is just a tool. Like most tools, it’s meant to save you on the effort expended, not to create more work for yourself. Inference should be preferred over explicit typings, except in places where it is an API boundary that you want to “lock in” how strict or permissive it is (from exact type to minimal duck-typing compatible protocol).

In the case of empty __slots__, inference is fine, but if you need to type it, you can type all correct uses of slots as Iterable[str]. All tools that handle slots need to handle the case where the attribute isn’t even there, as well as multiple valid forms, all of which are Iterable[str], the valid use of slots is covered in the data model, and there is no advantage to being more specific in this case. This also means if you later add a slot member, you don’t have to change the type.

I generally think that most cases of the empty tuple, and even some uses of non-empty tuples fall into a similar category where Iterable or Sequence is a more correct type. The exception is how some tensor libraries allow it (and only the empty tuple this way), attaching some special meaning to the empty tuple usage.


Same as this one

x = int, int # or (int, int)
x = int,  # or (int,)
x = ()

The difference is when the parens and commas are mandatory, and when things are implicitly tuples, which comes down to the language’s grammar and valid syntax. Often parens and a trailing comma are allowed but are implicit if not explicitly included. Generally, if it would be unambiguous to omit them, you may.

One could argue that that may have cut off various options, but I think most people would agree that they wouldn’t want to need parens everywhere that they are currently optional to save 2 characters in this specific case.

One could similarly argue that describing Python’s types in Python was a mistake and that the type system should have had a grammar to itself. However, this would have stifled or at least made it significantly harder to implement many uses of runtime introspection that have come up since.

What exists is largely the result of decisions that were, at the time they were made, pragmatic. Many things could have been different with the benefit of hindsight, but changing many of those now is just too disruptive for not enough benefit, and the effort of improving either expression or ergonomics is largely better spent on things that don’t even have solutions currently.

10 Likes

The problem comes down to the fact that Python’s generic typing system was unfortunately an afterthought to a well-established language and was made to piggyback on the existing subscript syntax for what is really meant to be an alternative __call__ method.

Had the generic typing system been designed at the same time the subscript syntax was, it would be extremely easy to make the case for a generalization of variable arguments of a call into variable arguments particularly for a variadic generic type.

That is, to extend the behavior of this:

def func(*args):
    ...

func(a, b) # func.__call__(a, b) => args = [a, b]
func(a) # func.__call__(a) => args = [a]
func() # func.__call__() => args = []

to _generic_class_getitem(cls, args):

class Array[*Shape]:
    ...

a: Array[int, int] # Array.__class_getitem__((int, int)) => args = (int, int)
a: Array[int] # Array.__class_getitem__((int,)) => args = (int,)
a: Array[] # Array.__class_getitem__(()) => args = ()

So while I can fully support such a generalization if we were in Python’s early days, I am neutral to the proposal now since we’re talking about a change to a well-established syntax for a really rare use case of an empty generic.