0

Say I have the following simple example without any typehints:

def wrapper(cls):
    class Subclass(cls):
        def subclass_method(self):
            pass
    return Subclass


@wrapper
class Parent:
    def parent_method(self):
        pass


p = Parent()
p.parent_method()
p.subclass_method()

How can I restructure this code using typehints, such that when I run mypy against an instance of Parent, it will recognize both subclass_method and parent_method?

Possible solutions:

  • Using a mixin Parent(Mixin): Works, but avoids the decorator. Is it possible to achieve without?
  • Patching the method onto the existing class: Still has the same issue of resolving subclass_method in mypy
  • Custom Mypy plugin: Wouldn't be sure where to start with this one, or if it would be possible without one.
flakes
  • 21,558
  • 8
  • 41
  • 88
  • Have you tried something like `def wrapper(cls) -> Callable[[...], "Subclass"]:`. Maybe you'd have to move the class definition outside the decorator. – user2390182 Feb 05 '21 at 15:08
  • @schwobaseggl Yeah that's kind of what I'm thinking, but I imagine `Subclass` would have to extend some `Generic[T]` to then have `parent_method` still be validated. I'm thinking this might require some sort of stub `.pyi` file. – flakes Feb 05 '21 at 15:10
  • What's the real-world use case for such a wrapper? Seems more complicated than defining `Subclass` *once* as a mix-in and using (multiple) inheritance directly. – chepner Feb 05 '21 at 15:15
  • Every class you decorate has a *different* class providing `subclass_method`, making your class hierarchy unnecessarily bloated. – chepner Feb 05 '21 at 15:16
  • @chepner I'm using the `dataclasses-json` library from pypi, and they provide a decorator `@dataclass_json` which adds some methods to the class methods for decoding/ encoding to json. The annoying part is that these methods do not show up in mypy. I agree the mixin approach is probably the easiest solution, but for curiosity sake, I would like to see how this might be achieved using the decorator. – flakes Feb 05 '21 at 15:23
  • 1
    Once [Intersection](https://github.com/python/typing/issues/213) gets implemented, you could move the mixin class to the global scope and then use the type hint `Intersection[T, SomeMixin]`. At the moment that's not possible though. – a_guest Feb 15 '21 at 21:51
  • @a_guest Yeah I was looking at that open issue. It seems like it could be the missing piece here! – flakes Feb 15 '21 at 21:56

1 Answers1

2

This would be much simpler without the wrapper at all.

class SomeMixin:
    def subclass_method(self):
        pass


class Parent(SomeMixin):
    def parent_method(self):
        pass


p = Parent()
p.parent_method()
p.subclass_method()

Here, you define SomeMixin once, not once per call to a wrapper, and the class SomeMixin is known statically. All the various classes with the name Subclass are created dynamically, and mypy can't know statically which class the name Parent is actually bound to.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • I think this is a good compromise, but I would still like to see if the decorator approach is possible! At the end of the day, I'd just like to see `@wrapper class Parent: pass` be able to resolve the `subclass_method` method. – flakes Feb 05 '21 at 15:25
  • The decorator approach is fundamentally at odds with `mypy`. `mypy` checks *static* typing, while the decorator makes *dynamic* changes to the type of the value bound to `Parent`. – chepner Feb 05 '21 at 15:26
  • If you really want to use a decorator, I would suggest writing one that adds a new method *directly* to the decorated class, rather than creating a new class at run-time via inheritance. – chepner Feb 05 '21 at 15:27
  • I find that a little bit surprising seeing as `@dataclass` is supported, but perhaps that is fairly special-cased in mypy? – flakes Feb 05 '21 at 15:28
  • `dataclass` doesn't change the static type. It heavily patches the original class rather than creating a new class that inherits from the original class (which is along the lines of what I was suggesting when you mentioned `dataclass`). – chepner Feb 05 '21 at 15:30
  • Interesting, so if using the decorator, patching is probably the way to go. But wouldn't those patched methods also be dynamically set? Shouldn't it have the same issue for resolving the extra methods? – flakes Feb 05 '21 at 15:35
  • I don't know exactly how `mypy` does its thing; it may be statically simulating the MRO, which `dataclass` doesn't change but your decorator does. – chepner Feb 05 '21 at 15:36
  • I agree with you, and the mixin approach is what I'm currently using from dataclasses-json to satisfy mypy: https://lidatong.github.io/dataclasses-json/#approach-2-inherit-from-a-mixin I do see that mypy supports custom plugins, which may be where this question is leaning. – flakes Feb 05 '21 at 15:46
  • this answer doesn't address the issue when decorators have to be used, *and* they are coming from an external package/library. – zerohedge Oct 11 '22 at 22:43