This is slightly revised version of something I sent to Stefano two 
weeks ago. I hope he is planning to use this, or something similar, in 
the PEP, but for what it's worth here it is for discussion.

This is, as far as I can tell, the minimum language change needed to 
support keywords in subscripts, and it will support all the desired 
use-cases.


* * * 


(1) An empty subscript is still illegal, regardless of context.

    obj[]  # SyntaxError


(2) A single subscript remains a single argument:

    obj[index]
    # calls type(obj).__getitem__(index)

    obj[index] = value
    # calls type(obj).__setitem__(index, value)

    del obj[index]
    # calls type(obj).__delitem__(index)

(This remains the case even if the index is followed by keywords; see 
point 5 below.)


(3) Comma-seperated arguments are still parsed as a tuple and passed as 
a single positional argument:

    obj[spam, eggs]
    # calls type(obj).__getitem__((spam, eggs))

    obj[spam, eggs] = value
    # calls type(obj).__setitem__((spam, eggs), value)

    del obj[spam, eggs]
    # calls type(obj).__delitem__((spam, eggs))


Points (1) to (3) mean that classes which do not want to support keyword 
arguments in subscripts need do nothing at all. (Completely backwards 
compatible.)


(4) Keyword arguments, if any, must follow positional arguments.

    obj[1, 2, spam=None, 3)  # SyntaxError

This is like function calls, where intermixing positional and keyword
arguments give a SyntaxError.


(5) Keyword subscripts, if any, will be handled like they are in 
function calls. Examples:

    # Single index with keywords:

    obj[index, spam=1, eggs=2]
    # calls type(obj).__getitem__(index, spam=1, eggs=2)

    obj[index, spam=1, eggs=2] = value
    # calls type(obj).__setitem__(index, value, spam=1, eggs=2)

    del obj[index, spam=1, eggs=2]
    # calls type(obj).__delitem__(index, spam=1, eggs=2)


    # Comma-separated indices with keywords:

    obj[foo, bar, spam=1, eggs=2]
    # calls type(obj).__getitem__((foo, bar), spam=1, eggs=2)

    and *mutatis mutandis* for the set and del cases.


(6) The same rules apply with respect to keyword subscripts as for 
keywords in function calls:

- the interpeter matches up each keyword subscript to a named parameter 
  in the appropriate method;

- if a named parameter is used twice, that is an error;

- if there are any named parameters left over (without a value) when the 
  keywords are all used, they are assigned their default value (if any);

- if any such parameter doesn't have a default, that is an error;

- if there are any keyword subscripts remaining after all the named
  parameters are filled, and the method has a `**kwargs` parameter,
  they are bound to the `**kwargs` parameter as a dict;

- but if no `**kwargs` parameter is defined, it is an error.


(7) Sequence unpacking remains a syntax error inside subscripts:

    obj[*items]

Reason: unpacking items would result it being immediately repacked into 
a tuple. Anyone using sequence unpacking in the subscript is probably 
confused as to what is happening, and it is best if they receive an 
immediate syntax error with an informative error message.


(8) Dict unpacking is permitted:

    items = {'spam': 1, 'eggs': 2}
    obj[index, **items]
    # equivalent to obj[index, spam=1, eggs=2]


(9) Keyword-only subscripts are permitted:

    obj[spam=1, eggs=2]
    # calls type(obj).__getitem__(spam=1, eggs=2)

    del obj[spam=1, eggs=2]
    # calls type(obj).__delitem__(spam=1, eggs=2)

but note that the setter is awkward since the signature requires the 
first parameter:

    obj[spam=1, eggs=2] = value
    # wants to call type(obj).__setitem__(???, value, spam=1, eggs=2)

Proposed solution: this is a runtime error unless the setitem method 
gives the first parameter a default, e.g.:

    def __setitem__(self, index=None, value=None, **kwargs)

Note that the second parameter will always be present, nevertheless, to 
satisfy the interpreter, it too will require a default value.

(Editorial comment: this is undoubtably an awkward and ugly corner case, 
but I am reluctant to prohibit keyword-only assignment.)


Comments
--------

(a) Non-keyword subscripts are treated the same as the status quo, 
giving full backwards compatibility.


(b) Technically, if a class defines their getter like this:

    def __getitem__(self, index):

then the caller could call that using keyword syntax:

    obj[index=1]

but this should be harmless with no behavioural difference. But classes 
that wish to avoid this can define their parameters as positional-only:

    def __getitem__(self, index, /):


(c) If the method is declared with no positional arguments (aside from 
self), only keyword subscripts can be given:

    def __getitem__(self, *, index)
    # requires obj[index=1] not obj[1]

Although this is unavoidably awkward for setters:

    # Intent is for the object to only support keyword subscripts.
    def __setitem__(self, i=None, value=None, /, *, index)
        if i is not None:
            raise TypeError('only keyword arguments permitted')


Gotchas
-------

If the subscript dunders are declared to use positional-or-keyword 
parameters, there may be some surprising cases when arguments are passed 
to the method. Given the signature:

    def __getitem__(self, index, direction='north')

if the caller uses this:

    obj[0, 'south']

they will probably be surprised by the method call:

    # expected type(obj).__getitem__(0, direction='south')
    # but actually get:
    obj.__getitem__((0, 'south'), direction='north')


Solution: best practice suggests that keyword subscripts should be 
flagged as keyword-only when possible:

    def __getitem__(self, index, *, direction='north')

The interpreter need not enforce this rule, as there could be scenarios 
where this is the desired behaviour. But linters may choose to warn 
about subscript methods which don't use the keyword-only flag.


-- 
Steve
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/OFUWFPI46UYOW5T3SYZ46HB73VYSQDJF/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to