pytype icon indicating copy to clipboard operation
pytype copied to clipboard

__new__ in tuple subclasses

Open doerwalter opened this issue 5 years ago • 4 comments

pytype issues a wrong-arg-count message for the following Python code:

class Color(tuple):
	def __new__(cls, r=0x0, g=0x0, b=0x0, a=0xff):
		return tuple.__new__(cls, (r, g, b, a))

	def witha(self, a):
		(r, g, b, olda) = self
		return self.__class__(r, g, b, a)

The output looks like this:

$ pytype pytypebug.py
Computing dependencies
Analyzing 1 sources with 0 local dependencies
ninja: Entering directory `/Users/walter/.pytype'
[1/1] check pytypebug
FAILED: /Users/walter/.pytype/pyi/pytypebug.pyi
/Users/walter/pyvenvs/default/bin/python3 -m pytype.single --imports_info /Users/walter/.pytype/imports/pytypebug.imports --module-name pytypebug -V 3.7 -o /Users/walter/.pytype/pyi/pytypebug.pyi --analyze-annotated --nofail --quick /Users/walter/pytypebug.py
File "/Users/walter/pytypebug.py", line 7, in witha: Function Color.__init__ expects 1 arg(s), got 5 [wrong-arg-count]
         Expected: (self)
  Actually passed: (self, _, _, _, _)

For more details, see https://google.github.io/pytype/errors.html#wrong-arg-count.
ninja: build stopped: subcommand failed.

However this is perfectly valid Python code:

>>> from pytypebug import *
>>> c = Color(0x33, 0x66, 0x99, 0xcc)
>>> c.witha(0xff)
(51, 102, 153, 255)

doerwalter avatar Jan 27 '20 10:01 doerwalter

This happens because pytype's builtins file defines tuple.__init__ instead of tuple.__new__: https://github.com/google/pytype/blob/9e315b940720c5734bcf8c7d683c310202938199/pytype/pytd/builtins/3/builtin.pytd#L555.

This has come up before internally, and we weren't able to fix it at the time because we couldn't figure out the right way to define __new__. If it's written as, say, def __new__(cls, Iterable[_T]) -> tuple[_T]: ... then subclasses of tuple will be incorrectly modeled as returning instances of tuple rather than themselves. But if we use: def __new__(cls: Type[_T], ...) -> _T: ... then tuple(...) calls will be modeled as returning Tuple[Any, ...], when we can currently infer a more specific element type.

rchen152 avatar Jan 27 '20 17:01 rchen152

Of course I call always annotate __new__ with:

def __new__(cls, r=0x0, g=0x0, b=0x0, a=0xff) -> "Color":

doerwalter avatar Jan 29 '20 14:01 doerwalter

Is there a workaround for this issue?

adarob avatar Sep 17 '21 21:09 adarob

I would probably just disable the wrong-arg-types error, but if you'd like to work around the issue without disabling type-checking, something like this should work:

import typing

class Color(tuple):
  def __new__(cls, r=0x0, g=0x0, b=0x0, a=0xff):
    return tuple.__new__(cls, (r, g, b, a))

  if typing.TYPE_CHECKING:
    def __init__(self, *args, **kwargs):
      pass

  def witha(self, a):
    (r, g, b, olda) = self
    return self.__class__(r, g, b, a)

(This tricks pytype into thinking there exists an __init__ method with the signature it expects. At runtime, TYPE_CHECKING evaluates to False.)

rchen152 avatar Sep 17 '21 23:09 rchen152