On Sun, Sep 27, 2020 at 4:29 AM Stefano Borini <stefano.borini@gmail.com> wrote:
>>> obj[**d] = "foo"  # no kwd arguments provided here
I committed yesterday the following proposal


But to be honest I am not sure if we should disallow these two constructs

as equivalent to the disallowed `d[]` or allow them as equivalent to
`d[()]` (or whatever the sentinel will be)

I have thought extensively about this issue (in fact I lie awake thinking it through last night :-) and have come to some kind of resolution.

TL;DR: these should be allowed and use `d[()]` or whatever the sentinel will be, but I prefer the sentinel to be `()`.

But there are more cases than just the above two. Below I try to catch them all.

We've already established that in the absence of `*a` and keyword args, the index is a tuple when it looks like a tuple:
d[x]          x
d[x,]         (x,)
d[x, y]       (x, y)
We've also established that in the absence of `*a` but with at least one keyword arg, the index is a tuple unless there is exactly one index value:
d[x, k=z]     x        {"k": z}
d[x, y, k=z]  (x, y)   {"k": z}
I propose to treat `*a` in a similar fashion: If _after expansion of `*a`_ there is exactly one index value, the index is that value, otherwise it's a tuple. IOW:
d[x, *[]]     x
d[x, *[y]]    (x, y)
d[*[], x]     x
d[*[y], x]    (y, x)
d[*[]]        ()
d[*[x]]       x
d[*[x, y]]    (x, y)
Note that I use `*[...]` instead of `*(...)` -- since `*a` takes an arbitrary iterable, it doesn't matter whether `a` is a list, tuple, another type of sequence, or a general iterable (e.g. a set or dict, or a generator). I'm using `*[...]` consistently just to remind us of this fact (and to avoid having to type a trailing comma to create a singleton tuple).

We can easily extend this scheme to keyword args: As soon as either a keyword argument or `**kwargs` (or both) is present, we apply the same rule: If _after expansion of `*a`_ there is exactly one index value, the index is that value, otherwise it's a tuple. I'm not going to show all the cases, but here are some examples:
d[*[], k=z]       ()       {"k": z}
d[*[x], k=z]      x        {"k": z}
d[*[x, y], k=z]   (x, y)   {"k": z}
d[*[], **{}]      ()       {}
d[*[x], **{}]     x        {}
d[*[x, y], **{}]  (x, y)   {}
I propose to then treat the cases where there are no positional index values, only keywords (either `k=1` or `**{...}`, even `**{}`) _as if preceded by `*[]`_. So:
d[k=z]            ()       {"k": z}
d[**{"k": z}      ()       {"k": z}
d[**{}]           ()       {}
The reason for these choices is to minimize the number of inconsistencies. We have a few unavoidable inconsistencies:

- if there's only one index value the index is not a tuple, in all other cases it's a tuple -- backward compatibility
- the special case for `d[x,]` (not the same as `d[x]`) -- also backward compatibility
- the difference between `d[x,]` (index is a tuple) and `d[x, k=1]` (index not a tuple) -- user expectations

There's also the special case for `d[k=1]`. Here our choices are to either forbid it syntactically or to provide a sentinel. I think forbidding it will prevent some reasonable use cases (e.g. tables with columns that have both positions and names). So I think it's best to use a sentinel. Using `()` as the sentinel reduces the number of special cases: the rule becomes "if there's exactly one positional value, use that value as the index; otherwise use a tuple", while with another sentinel the rule would become "if there's more than one positional value, use a tuple, if there's exactly one use that value, else (there are no positional values) use the sentinel". But if in the end people prefer a sentinel other than `()`, I can live with that -- in all the above cases, just replace `()` with the selected sentinel.

In both cases we also have an exception for the form `d[x,]`, but this factors out because it's the same complication in each case. Note that this exception is unique -- it only applies if the syntactic form has no keywords, no `**kwargs`, and no `*args`.

The introduction of `*args` requires us to decide what to do with the edge cases. I think the rule that best matches user expectations is to combine the plain positional values with the expansion of `*args`, take the resulting sequence of values, and _then_ apply the rule from the previous paragraph (using whichever sentinel we end up deciding on).

The introduction of `**kwargs` should pose no extra difficulties. Again, if there are no positional index values the sentinel index is used, and syntactic keywords are combined with `**kwargs` to form a single dict of keyword args. We end up finding that `d[**kwargs]` uses the index sentinel regardless of whether `kwargs` is empty or not. (If we were to end up forbidding `d[k=1]` syntactically, the only consistent choice would be to raise for `d[**{}]`, but I don't see a good reason to go this way.)

Note that `*args` and `**kwargs` both must combine the hard-coded arguments of the same nature (positional or keyword) with the dynamic ones before deciding. Anything else would lead to more rules and more inconsistencies.

Finally, in the above examples, `x`, `y` and `z`, when occurring in hard-coded arguments of either nature, may also be slices, e.g. `d[x]` could be `d[i:j]` or `d[i:j:k]` or e.g. `d[::]`. This makes no difference for the analysis. Note that in `*args` and `**kwargs` the slice notation is not syntactically valid -- but you can use explicit calls to `slice()`, e.g. `slice(i, j)`, `slice(i, j, k)` or `slice(None, None, None)`.

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