Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to annotate attribute that can be implemented as property?

I am trying to make mypy happy with my type annotations. Here is minimal example:

class FooInterface:
    x: int


class FooWithAttribute(FooInterface):
    x: int = 0


class FooWithProperty(FooInterface):
    @property
    def x(self) -> int:
        return 0

To my human understanding everything is fine: both FooWithAttribute().x and FooWithProperty().x will return 0 which is int, no type errors. However mypy complains:

error: Signature of "x" incompatible with supertype "FooInterface"

Is there a way to tell mypy that everything is OK? Right now the only way I found is annotating x: typing.Any in FooInterface which wastes the information that x is int.

like image 792
sanyassh Avatar asked Oct 11 '19 23:10

sanyassh


People also ask

How do you use property annotations in Java?

The @Property annotation can be used on fields, on setter methods or on a constructor method parameter. However, the @Property annotation MUST NOT be used on a class field that is declared as final. Properties can also be injected via setter methods even when the @Property annotation is not present.

What does @property mean Python?

The @property is a built-in decorator for the property() function in Python. It is used to give "special" functionality to certain methods to make them act as getters, setters, or deleters when we define properties in a class.


1 Answers

Mypy is actually pointing out a legitimate bug in your program. To demonstrate, suppose you have a program that looks like this:

def mutate(f: FooInterface) -> None:
    f.x = 100

Seems fine, right? But what happens if we do mutate(FooWithProperty())? Python will actually crash with an AttributeError!

Traceback (most recent call last):
  File "test.py", line 19, in <module>
    mutate(FooWithProperty())
  File "test.py", line 16, in mutate
    f.x = 100
AttributeError: can't set attribute

To make mypy happy, you basically have two options:

  1. Make FooInterface.x also be a read-only property
  2. Implement a setter for FooWithProperty.x to make it writable

I'm guessing that in your case, you probably want to take approach 1. If you do so, mypy will correctly point out that the line f.x = 100 is not permitted:

from abc import abstractmethod

class FooInterface:
    # Marking this property as abstract is *optional*. If you do it,
    # mypy will complain if you forget to define x in a subclass.
    @property
    @abstractmethod
    def x(self) -> int: ...

class FooWithAttribute(FooInterface):
    # No complaints from mypy here: having this attribute be writable
    # won't violate the Liskov substitution principle -- it's safe to
    # use FooWithAttribute in any location that expects a FooInterface.
    x: int = 0

class FooWithProperty(FooInterface):
    @property
    def x(self) -> int:
        return 0

def mutate(f: FooInterface) -> None:
    # error: Property "x" defined in "FooInterface" is read-only
    f.x = 100

mutate(FooWithProperty())

Approach 2 unfortunately doesn't quite work yet due to a bug in mypy -- mypy doesn't correctly understand how to handle overriding an attribute with a property. The workaround in this case is to make FooInterface.x a property with a setter.

like image 131
Michael0x2a Avatar answered Sep 18 '22 04:09

Michael0x2a