3

I have the following inheritance structure:

class S:
    ...

class A(S):
    ...

class B(S):
    ...

I'd like to conceptually do something like the following:

class Foo:
    T = TypeVar('T', bound=S)

    items: dict[type[T], T] = dict()

    def get(self, t: type[T]) -> T:
        return self.items[t]

In words, I have a dict that maps from subtypes (A, B) to instances of those subtypes (A(), B()). I want to type hint a method that wraps this dictionary, and I want static analysis to show that it always returns an instance of the input type specifically, not just an instance of the supertype.

How can I type hint this properly?

5
  • This question is similar to: Type hint for a dict that maps tuples containing classes to the corresponding instances. If you believe it’s different, please edit the question, make it clear how it’s different and/or how the answers on that question are not helpful for your problem. Commented Sep 15 at 12:00
  • @InSync I don't know if this is the same...OP here doesn't necessarily need to type hint the dict, just the method. Commented Sep 15 at 17:57
  • @Anerdw The principle is the same, regardless. Commented Sep 15 at 19:50
  • Hi InSync, thanks for providing the related question. While there's technical similarity I feel that the inclusion of scope into this use case is a unique attribute to the problem. The fact that I'm trying to infer the specialized return type from that of a parameter, while having an outer scope referring to that same type (without e.g. parameterizing the class) seems to make this use case distinct from that your suggested question addresses. I'm happy to clarify that angle in my question above. Commented Sep 20 at 14:01
  • P.S.: My initial wording pointed that distinction out while the first edit (Anerdw's) seems to have dropped that detail. Commented Sep 20 at 14:06

2 Answers 2

2

I don't think this will play well with a direct dict type hint. If you only want the client to use get, not access items directly, you could use some behind-the-scenes casting - it'll look indistinguishable from a "clean" solution to the end user.

The following works on mypy and Pyright:

from typing import cast, reveal_type

class S:
    ...
class A(S):
    ...
class B(S):
    ...

class Foo:
    _items: dict[type[S], S] = {A: A(), B: B()}

    def get[T: S](self, t: type[T]) -> T:
        return cast(T, self._items[t])

foo = Foo()
reveal_type(foo.get(A)) # A
reveal_type(foo.get(B)) # B

You could also define your own type-instance mapping class that supports this, but I don't really see a reason to do so.

Sign up to request clarification or add additional context in comments.

3 Comments

Terrific, that's what I was looking for. I had considered casting but abandoned it as Python's typing system (or rather the static type checker) cannot (or just does not?) verify if a cast is possible. If the value type of _items changes into one that cannot be cast to T, strictly speaking, it won't complain as far as I'm aware. Can you confirm that or am I possibly overlooking something?
@bossi Yeah, that's about right. This option will make sure the client doesn't mess up their code, but it won't really do the same for you.
Thanks for confirming, Anerdw. I think I figured out a way to do this without having to use a cast, thus inhibiting type inference. I might post a separate question for clarity, building upon what you've contributed.
0

Building upon Anerdw's solution I figured out how to do this without the following caveat: While his solution is 80% there it falls short on providing full type inference as the cast circumvents it. While it's only an assumption that the cast would be possible the code might change without a static type checker being able to detect the conflict.

This solution builds upon his suggestion but does so without casting, thus retaining full type inference:

from typing import cast, reveal_type, TypeAlias, TypeVar


class S:
    ...
class A(S):
    ...
class B(S):
    ...


class X:
    ...


T = TypeVar('T', bound=S)
Ta: TypeAlias = dict[type[T], T]


class Foo:
    items: Ta = dict()

    def get(self, t: type[T]) -> T:
        return self.items[t]

reveal_type(Foo().get(A))  # Type of "Foo().get(A)" is "A"

4 Comments

Have you tested this? I don't think it'll fix the inference problem. For me, setting items: Ta = {int: 3, str: 3} passes mypy type-checking; T gets inferred to be Any.
Yes, reveal_type(Foo.items) evaluates to Unknown for me and setting invalid types to the dictionary passes, the way the above currently stands. This is on Pylance's standard profile. Strict points out that items is partially unknown; something's probably a bit off there still, strict also points out that Ta in items: Ta expects a type argument, but setting that to Ta[S] causes errors back in get, saying that S can't be resolved to T, so it's a bit of a circle jerk. Ideally you'd go items: Ta[T] (or directly w/out an alias) but T has no meaning in that scope.
However, Foo().get(A) does indeed resolve to A, while Foo().get(int) errors out and correctly says that int cannot be resolved to S. This is the main point of interest for my use case to make sure the inference between argument and return type of foo is checking correctly and the way it stands above does not produce any error (Pylance standard) with the caveat that Foo.items is not type checked (even though somehow it seems to be, internally..?!), and Foo.items[int] = 3 does not error. So, in a way, the cast's blindfold has just been passed on. I wonder if there's a way.
I can't quite figure out a better way and your solution is probably the cleanest while it actually does notify if underlying types (items) change as we've bound T of get to S, unless I'm missing something (as opposed to what I was saying in my very first comment). The only caveat is that something like Foo.items[B] = A() is permitted as long as we cannot use the same generic on both, get as well as items. I'll give it some more time, then probably take this answer down again.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.