On Sun, Aug 16, 2020 at 5:45 AM Steven D'Aprano <steve@pearwood.info> wrote:
On Mon, Aug 17, 2020 at 12:32:08AM +1200, Greg Ewing wrote:
> On 16/08/20 11:49 am, Guido van Rossum wrote:
> >    I can see two basic ways of allowing no arguments. One is for the
> >    interpreter to construct an object that is the argument passed to
> >    __getitem__ and so forth. The other is to not pass an argument at
> >    all. I see this as a secondary question.
> If d[] were to be allowed, I would expect it to pass an empty
> tuple as the index, since it's the limiting case of reducing the
> number of positional indices.

So you would expect `obj[]` and `obj[()]` to be the same?

That's not terrible, since these are also the same:
obj[x] === obj[(x)]
obj[x, y] === obj[(x, y)]
(Even though `obj[x]` is still an exception because it's the only form that isn't tuplified.)

On the other hand, it's *also* the limiting case of reducing the number of keyword arguments, so whatever is passed here should also be passed as the positional part of the key when only keyword arguments are present, and I'm not sure what I think of using `()` for that.
Personally, I think that unless there is an overwhelmingly good use-case
for an empty subscript, we should continue to treat empty subscripts
(no positional or keyword arguments) as a syntax error.

That's where I am too right now. But I think there may be at least a _decent_ use case: the `Tuple` type in type annotations. (And since PEP 585 also the `tuple` type.)

We have `Tuple[int, int]` as a tuple of two integers. And we have `Tuple[int]` as a tuple of one integer. And occasionally we need to spell a tuple of *no* values, since that's the type of `()`. But we currently are forced to write that as `Tuple[()]`. If we allowed `Tuple[]` that odd edge case would be removed.

So I probably would be okay with allowing `obj[]` syntactically, as long as the dict type could be made to reject it.

Alas, I thought I had a solution, but it doesn't work for `__setitem__`: we can easily state that `obj[]` calls `obj.__getitem__()` and whether that's accepted or not depends on whether `obj.__getitem__` has a default value for its `key` positional argument. But what to do for `obj[] = x`? We can't call `obj.__setitem__(x)` -- well, we could, but it would be super ugly to write such a `__getitem__` method properly -- similar to supporting `range(n)`.

So my intuition is failing me. It looks like `d[] = x` will need to come up with *some* key, and the only two values that sound at all reasonable are `()` (for the reason Greg mentioned) and `None` (because it's the universal "nothing here" value). But either way it's not reasonable for `dict` to reject those keys -- they are legitimate keys when passed explicitly. Using `()` is slightly better because it helps debugging: if you have a dict with an unexpected `None` key you should look for a key computation that unexpectedly returned `None`, and we can now add that if you have a dict with an unexpected `()` key, you should look for an assignment of the form `d[] = x`.

But it would be better if `d[] = x` could be simply rejected -- either at runtime (perhaps with a TypeError, like for calling a function with insufficient arguments), or syntactically. (That is, if you believe, like me, that `d[key, kwd=val]` should be rejected.) Hence, `Tuple[]` qualifies as a decent use case, but not as an overwhelmingly good one.

I can think of another way to deal with this -- we could define a new sentinel object (e.g. `Nope` :-), `d[]` could be equivalent to `d[Nope]`, and the dict class could reject `Nope` as a key value. But that's quite ugly, and it's arbitrary, too.

PS. All this reminds me of a complaint I heard 4-5 decades ago from an experienced programmer when I was just learning the ropes, and which somehow stuck in my mind ever since: "... and yet again, the empty [sequence] is treated rather shabbily." (It sounded better in Dutch -- "stiefmoederlijk", meaning "stepmotherly".)

--Guido van Rossum (python.org/~guido)