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

TypeVar bound doesn't understand CRTP #17613

Open
jgarvin opened this issue Jul 31, 2024 · 2 comments
Open

TypeVar bound doesn't understand CRTP #17613

jgarvin opened this issue Jul 31, 2024 · 2 comments
Labels
bug mypy got something wrong

Comments

@jgarvin
Copy link

jgarvin commented Jul 31, 2024

Bug Report

Ideally, this snippet would work. It enables making multiple types that share the same implementation but don't subclass each other (like newtype in other languages). If you comment out the other.quack() call and the bound="Foo[DerivedT]") then it will typecheck -- the issue is specifically with being able to express a typevar bound indicating the typevar has a base class that uses that typevar as a generic parameter.

#!/usr/bin/env python

from __future__ import annotations
from typing import Generic, TypeVar

DerivedT = TypeVar("DerivedT", bound="Foo[DerivedT]")

class Foo(Generic[DerivedT]):
    def quack(self) -> None:
        print("quack!")

    def quack_together(self, other: DerivedT) -> None:
        self.quack()
        other.quack()

class Bar(Foo[Bar]): pass

x = Bar()
y = Bar()

x.quack_together(y)
$ python -m mypy --strict /tmp/self_test.py 
self_test.py:6: error: Type variable "self_test.DerivedT" is unbound  [valid-type]
self_test.py:6: note: (Hint: Use "Generic[DerivedT]" or "Protocol[DerivedT]" base class to bind "DerivedT" inside a class)
self_test.py:6: note: (Hint: Use "DerivedT" in function signature to bind "DerivedT" inside a function)
Found 1 error in 1 file (checked 1 source file)
  • Mypy version used: mypy 1.10.0 (compiled: yes)
  • Mypy configuration options from mypy.ini (and other config files): N/A
  • Python version used: 3.12
@jgarvin jgarvin added the bug mypy got something wrong label Jul 31, 2024
@jgarvin
Copy link
Author

jgarvin commented Jul 31, 2024

The closest related issue is #11063 where it looks like the author separately ran into this issue because they use Base[Any] as the bound instead of Base[T].

@jgarvin
Copy link
Author

jgarvin commented Jul 31, 2024

An imperfect workaround for anyone who stumbles on this later:

  • Make the bound Base[Any].
  • Change all methods from operating on DerivedT to operating on Base[DerivedT] (this is mostly okay since we assume the only class that satisfies this is DerivedT)
  • If you need to construct an instance of the derived, do type(self)(args)
  • Anywhere you return a type(self)(args) instead of returning DerivedT or Base[DerivedT] return Self -- this helps callers if you have any extra methods on DerivedT that they expect to be able to call on the object returned from your method

This gets you most of the way there without having to introduce a ton of casts and isinstance asserts in the internal code or for callers. But it would still be clearer if bound=Base[DerivedT] worked :)

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

No branches or pull requests

1 participant