Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mixins with conflicting signatures #2125

Closed
rowillia opened this issue Sep 12, 2016 · 9 comments
Closed

Mixins with conflicting signatures #2125

rowillia opened this issue Sep 12, 2016 · 9 comments

Comments

@rowillia
Copy link
Contributor

rowillia commented Sep 12, 2016

I'm finding it's quite common to have Python Libraries using mixins with signatures that mypy considers to be conflicting because one takes a different set of kwargs than the other. For example - https://github.com/pallets/itsdangerous/blob/master/itsdangerous.py#L881-L886 (which is simplified below).

class A(object):
    def load(self, payload):
        pass
class B(object):
    def load(self, payload, return_header=False):
        pass
    def dump(self, obj):
        pass
class C(A, B):
    pass

In this case, mypy will complain that A and B have conflicting signatures for load. In practice, Python will always dispatch load calls to A so C's signature for load should be load(self, payload). It would be incorrect to pass return_header to C().load.

What are your thoughts on having mypy reflect what Python will do at runtime here and just take the definition of A's load instead of throwing an error? The only way around this currently is to add **kwargs to As signature which is less safe.

@gvanrossum
Copy link
Member

gvanrossum commented Sep 12, 2016 via email

@rowillia
Copy link
Contributor Author

@gvanrossum I think that should be OK, right? We would assume the signature we're dispatching to, right? It's definitely not a great practice as it breaks the liskov substitution principle (List[C] is covariant with List[A] but is not with List[B]), but I'm finding it to be quite common in popular libraries.

I think it would make sense to use the same logic as mypy does for inheritance. For example, in the below code example mypy is OK with OKSubA overriding load since it's compatible with Base's signature.

class Base(object):
    def load(self, payload: str) -> None:
        pass
class OKSub(Base):
    def load(self, payload: str, something_else=False) -> None:
        pass
class BadSub(Base):
    def load(self, payload: int, something_else=False) -> None:
        pass

@gvanrossum
Copy link
Member

gvanrossum commented Sep 12, 2016 via email

@JukkaL
Copy link
Collaborator

JukkaL commented Sep 12, 2016

Mypy treats inheritance as subtyping. In the original example, C is not (safely) a subtype of B, and that's why mypy gives the warning. For example, what about code like this:

def f(b: B) -> None:
    b.load(whatever, return_header=True)   # Fails at runtime if b is an instance of C

f(C())   # Should this be okay?

See also python/typing#246 and python/typing#241, which are related.

@rowillia
Copy link
Contributor Author

@JukkaL Totally....This would be a case of weakening the type checker to support existing code. I don't think this is a great practice for the reasons you highlighted, but I've found it to be quite a common pattern in existing code bases. This seems to more arise from using Mixins for composition and code sharing as opposed to using Mixins to express a type hierarchy.

In the example above, it's safe to say instanceof(C(), A) == True but instanceof(C(), B) == False If we encounter a case of treating C as a B, we could warn and point to the declaration of B.load as the reason why C can't be treated as a B.

@JukkaL
Copy link
Collaborator

JukkaL commented Sep 13, 2016

It still wouldn't safe in general. Consider this example:

class A(object):
    def load(self, payload):
        pass
class B(object):
    def load(self, payload, return_header=False):
        pass
    def load2(self, payload):
        self.load(payload, return_header=True)
class C(A, B):
    pass

C().load2(stuff)  # ouch

I'm not convinced that mypy should silently accept such code, but it would be nice if there was a documented way of using the idiom with mypy. Here are some possibilities:

  1. Make sure # type: ignore used suitably does the right thing and document it.
  2. Support implementation-only inheritance (Allow subclassing without supertyping typing#241). This may be hard to get right, though.
  3. Support declaring B as "pure implementation" and type check it specially (Declaring classes as pure mixins typing#246).
  4. Have a more specific way of ignoring just these errors when doing multiple inheritance.

The # type: ignore way would probably be the simplest, and it's my favorite for now. We could have that together with warning about relying on C being a subtype of B. 2 and 3 above could be type safe, but they are more complicated and have various usability and performance concerns.

@gvanrossum
Copy link
Member

gvanrossum commented Sep 13, 2016 via email

pgjones added a commit to pgjones/werkzeug that referenced this issue May 10, 2020
This mostly adds mypy to the tox process to enable checking of any
types added (and code it understands). This will allow type hints to
be progressively added to the Werkzeug codebase, with the eventual aim
of turning on disallow_untyped_defs.

Type annotations are enforced as is the convention of only importing
typing required objects within a `if TYPE_CHECKING:` block. This
should limit the import time cost of type hinting.

Note that,
 - mypy doesn't understand conditional imports
   python/mypy#1153

 - mypy insists mixins with different kwarg signatures are errors,
   python/mypy#2125 (Response)
pgjones added a commit to pgjones/werkzeug that referenced this issue Jun 20, 2020
This mostly adds mypy to the tox process to enable checking of any
types added (and code it understands). This will allow type hints to
be progressively added to the Werkzeug codebase, with the eventual aim
of turning on disallow_untyped_defs.

Type annotations are enforced as is the convention of only importing
typing required objects within a `if TYPE_CHECKING:` block. This
should limit the import time cost of type hinting.

Note that,
 - mypy doesn't understand conditional imports
   python/mypy#1153

 - mypy insists mixins with different kwarg signatures are errors,
   python/mypy#2125 (Response)
@astrojuanlu
Copy link
Contributor

@hauntsaninja Would you please elaborate on the reasons to close this issue? Is it not present anymore?

@hauntsaninja
Copy link
Collaborator

hauntsaninja commented May 9, 2021

The resolution here seemed to be that this is unsafe in general, and type ignore is the best option here, especially if you could type ignore a specific message. mypy now supports type ignore with error codes. Also Protocols are a thing now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants