5

How can I provide type annotations for attributes which are defined via __slots__? E.g. for this class:

class DiffMatch:
  __slots__ = ["ref_seq_idx", "ref_txt", "hyp_txt", "start_time", "end_time"]

  def __repr__(self):
    return "%s(%s)" % (self.__class__.__name__, ", ".join(["%s=%r" % (a, getattr(self, a)) for a in self.__slots__]))

(Python 2 compatible in a comment if possible (otherwise Python 3 only is fine), and also such that PyCharm can handle it (actually that is most important for me now).)

Albert
  • 65,406
  • 61
  • 242
  • 386
  • Note that without any default values for those attributes, the `__repr__` method will throw an `AttributeError` on `getattr(self, a)`. – Martijn Pieters Sep 28 '18 at 10:35

3 Answers3

7

__slots__ only tells the type() object to make space for potential attributes. The names in the list are not themselves attributes. All you have is a bunch of descriptors. There are no values, so no types.

So you need to make actual attributes, and those have all the regular options for type annotations.

For Python 3.6 and up, use variable type annotations:

class DiffMatch:
    __slots__ = ["ref_seq_idx", "ref_txt", "hyp_txt", "start_time", "end_time"]

    ref_seq_idx: int
    # ...

or for versions before 3.6 (including Python 2), you have to annotate the types on a method that can set the attributes. There is no real instance attribute support otherwise. You can add a dummy method for this:

class DiffMatch:
    __slots__ = ["ref_seq_idx", "ref_txt", "hyp_txt", "start_time", "end_time"]

    def __type_hints__(self, ref_seq_idx, ...):
        """Dummy method to annotate the instance attribute types
        # type: (int, ...) -> None
        """
        self.ref_seq_idx = ref_seq_idx
        # ...

where all the types for each of the arguments are listed in the docstring. If you have a __init__ method for your class that also touches all the attributes, then a dummy method is not needed.

Note that you can't set class default values for these, which means that you can't use ref_seq_idx = None # type: int (pre-Python 3.6) or ref_seq_idx: int = None (Python 3.6 and up); __slots__ names are converted to descriptor objects on the class, so the names are already set.

Last but not least, I would take a serious look at the attrs library to build these types for you. Support for this library has recently been added to PyCharm 2018.2, so type information is automatically picked up:

@attr.s(slots=True)
class DiffMatch:
    ref_seq_idx = attr.ib(init=False)  # type: int
    # ...

and you'll get your __repr__ method generated for free. The init=False annotation tells attrs not to include that name in the __init__, at which point instances won't have that attribute set at all on instantiation.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Yes I know what `__slots__` does. With type annotations, I mostly meant just specifying a docstring which is recognized by PyCharm. But what you are saying is basically that I should just add sth like this in the class? `ref_seq_idx = None # type: int` *Edit* I just tried, but then it complains that this conflicts with class variable in `__slots__`. *Edit* Ok, doing the same thing in `__init__` works. – Albert Sep 28 '18 at 10:21
  • @Albert: yes, on Python versions without variable type annotations you can't set class defaults for slots, because those would clash with the descriptor objects with the same names that `__slots__` create. – Martijn Pieters Sep 28 '18 at 10:31
2

The attrs library mentioned in this answer looks pretty neat. Though, I believe the folowing code would be a way to achieve what you want without it:

class DiffMatch:
    ref_seq_idx: int
    ref_txt: str
    hyp_txt: str
    start_time: Decimal
    end_time: Decimal

    # __annotations__ is a dict and we can
    # construct __slots__ from its keys.
    __slots__ = tuple(__annotations__)
Mike R
  • 329
  • 2
  • 11
0

My solution now:

class DiffMatch:
  __slots__ = ["ref_seq_idx", "ref_txt", "hyp_txt", "start_time", "end_time"]

  def __init__(self):
    self.ref_seq_idx = None  # type: int
    self.ref_txt = None  # type: str
    self.hyp_txt = None  # type: str
    self.start_time = None  # type: Decimal
    self.end_time = None  # type: Decimal

  def __repr__(self):
    return "%s(%s)" % (self.__class__.__name__, ", ".join(["%s=%r" % (a, getattr(self, a)) for a in self.__slots__]))
Albert
  • 65,406
  • 61
  • 242
  • 386
  • Your empty `__init__` argument list and `repr()` output clash now. You can't recreate your `DiffMatch()` instance by passing in values, so your `repr()` should not pretend that it can. – Martijn Pieters Sep 28 '18 at 10:36
  • And on a separate note: you probably want to use a [`attrs` dataclass](http://www.attrs.org/en/stable/) for this; I added an example and further motivation to my answer. – Martijn Pieters Sep 28 '18 at 10:40