Search code examples
pythontuplessubclass

Backwards incompat change in subclassing between 3.6 and 3.7


Python 3.6

>>> class Tup(tuple):
...     pass
... 
>>> Tup(x=123)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'x' is an invalid keyword argument for this function

Python 3.7

>>> class Tup(tuple):
...     pass
... 
>>> Tup(x=123)
()

What changed between 3.6 and 3.7? And why?


Solution

  • The behavior in the newer versions of Python is a bug.

    The change between version 3.6 and 3.7 comes from how tuple handles its arguments. In 3.6 and earlier, the single optional argument to tuple could be passed either positionally (the normal, documented way), or as a keyword argument using the undocumented name sequence for the argument.

    >>> tuple(sequence="ab") # this only works on Python versions prior to 3.7
    ('a', 'b')
    

    Python 3.7 removed the ability to pass the argument as a keyword. Instead it became a positional-only argument. At the time, only builtin functions could have that kind of argument, though the syntax for them was documented in PEP 457 (and they became available to normal Python code in Python 3.8 which implemented PEP 570). You can see the positional-only argument syntax in the documentation for tuple in newer versions of Python, where the nature of the argument is subtly indicated by the addition of a slash in the constructor signature (after the single positional-only argument):

    >>> help(tuple) # in more recent versions of Python
    Help on class tuple in module builtins:
    
    class tuple(object)
     |  tuple(iterable=(), /)
    [...]
    

    Unfortunately the implementation of the change seems to have had some unintended consequences for the handling of keyword arguments in tuple subclasses, as the inherited constructor method silently ignores all keyword arguments it is passed, rather than raising an appropriate exception (as it did in 3.6 and before for any keyword name other than sequence).

    This issue has already been reported to the Python developers, on their bug tracking system (perhaps by you?). Once the best way to correct the issue is identified, I expect it will be fixed for the versions of Python that are still accepting bug fixes (3.8 and 3.9 currently, but not 3.7 which only gets security fixes). It may be a few months before it makes its way into a release.