3

Let's say I want to do define wrapper classes on sets and lists that add some useful methods, like this:

from abc import ABC

class AbstractGizmo(ABC):

    def bloviate(self):
        print(f"Let me tell you more about my {len(self)} elements")

class ListGizmo(list, AbstractGizmo):
    pass    

class SetGizmo(set, AbstractGizmo):
    pass

Now I can call:

>>> ListGizmo([1, 2, 3]).bloviate()
>>> SetGizmo({1, 2, 3}).bloviate()

But I also want to have bloviate() available on its own as a utility method:

from typing import Union, Set, List

def bloviate(collection: Union[Set, List]):
    print(f"Let me tell you more about my {len(collection)} elements")


class AbstractGizmo(ABC):

    def bloviate(self):
        return bloviate(self)

So I can also do:

>>> bloviate([1, 2, 3])
>>> bloviate({1, 2, 3})

Since subclass ListGizmo is a list, and subclass SetGizmo is a set, this setup actually works fine in practice. But static type checkers (like pyright) don't know that, so they (correctly) show an error here:

class AbstractGizmo(ABC):

    def bloviate(self):
        return bloviate(self)  # Error: Type 'AbstractGizmo' cannot be assigned
                               # to type 'Set[Unknown] | List[Unknown]'

Is there some way I can indicate to Python / pyright that, essentially, "all instances of AbstractGizmo are guaranteed to be in Union[Set, List]"? This syntax escapes me.

(Note that of course in this simple example I can just define bloviate() on each subclass to avoid the problem. In reality I have more methods and more wrapper subclasses, so I get a combinatorial explosion if I can't abstract them to AbstractGizmo.)

Sasgorilla
  • 2,403
  • 2
  • 29
  • 56

1 Answers1

0

To properly type mixin classes, annotate the self parameter as a Protocol matching the functionality of the desired base types:

from typing import Protocol
from abc import ABC

class HasLength(Protocol):  # or just `typing.Sized` in this case
    def __len__(self) -> int: ...

def bloviate(collection: HasLength):
    print(f"Let me tell you more about my {len(collection)} elements")

class AbstractGizmo(ABC):
    def bloviate(self: HasLength):
        return bloviate(self)

class ListGizmo(list, AbstractGizmo):
    pass

ListGizmo().bloviate()  # this is fine

Note that the mixin can still be combined with other types without raising a static type error. However, using the respective method triggers an error both furing runtime and static type checking.

class IntGizmo(int, AbstractGizmo):
    pass

IntGizmo().bloviate() # error: Invalid self argument "IntGizmo" to attribute function "bloviate" with type "Callable[[HasLength], Any]"
MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119