Marcos Schroh
Marcos Schroh

Reputation: 465

How to get the correct signatures order of annotations in methods when performing overriding

I am trying to fix some methods annotations on magic and normal methods. For example, I have some cases like:

```

class Headers(typing.Mapping[str, str]):
    ...

    def __contains__(self, key: str) -> bool:
        ...
        return False

    def keys(self) -> typing.List[str]:
        ...
        return ['a', 'b']

```

and when I run mypy somefile.py --disallow-untyped-defs I have the following errors:

error: Argument 1 of "__contains__" incompatible with supertype "Mapping" error: Argument 1 of "__contains__" incompatible with supertype "Container" error: Return type of "keys" incompatible with supertype "Mapping"

What I understand is that I need to override the methods using the @override decorator and I need to respect the order of inheritance. Is it correct?

If my assumption is correct, Is there any place in which I can find the exact signatures of the parent classes?

Upvotes: 0

Views: 669

Answers (1)

Marcos Schroh
Marcos Schroh

Reputation: 465

After asking the question on mypy, the answer was:

Subclassing typing.Mapping[str, str], I'd assume that the function signature for the argument key in contains ought to match the generic type?

contains isn't a generic method -- it's defined to have the type signature contains(self, key: object) -> bool. You can check this on typeshed. The reason why contains is defined this way is because doing things like 1 in {"foo": "bar"} is technically legal.

Subclassing def contains(self, key) to def contains(self, key: str) is in any case more specific. A more specific subtype doesn't violate Liskov, no?

When you're overriding a function, it's ok to make the argument types more general and the return types more specific. That is, the argument types should be contravariant and the return types covariant.

If we did not follow the rule, we could end up introducing bugs in our code. For example:

class Parent:
    def foo(self, x: object) -> None: ...

class Child(Parent):
    def foo(self, x: str) -> None: ...

def test(x: Parent) -> None:
    x.foo(300)  # Safe if 'x' is actually a Parent, not safe if `x` is actually a Child.

test(Child())

Because we broke liskov, passing in an instance of Child into test ended up introducing a bug.

Basically if I use Any for key on __contains__ method is correct and mypy won't complaint :

 def __contains__(self, key: typing.Any) -> bool:
    ...
    return False

You can follow the conversation here

Upvotes: 2

Related Questions