Generic type inference from another generic type (original) (raw)

Hello,
So i was wondering in the following code if there is a way to infer in Model2 the target type from the provider type.

from dataclasses import dataclass

@dataclass
class Provider[T]():
    field: T

    def get(self) -> T:
        return self.field

@dataclass
class Dependence[P: Provider[T], T]():
    provider: P

    def target(self) -> T:
        return self.provider.get()
    
# Way that works but need to provide twice the "int" type
@dataclass
class Model:
    dep: Dependence[Provider[int], int]

m = Model(Dependence(Provider(42)))
reveal_type(m.dep.target()) # Typed as int correctly


@dataclass
class Model2:
    dep: Dependence[Provider[int]]

m = Model2(Dependence(Provider(42)))
reveal_type(m.dep.target()) # Typed as Any 

I would like Dependence to be generic only over the Provider type and use the Provider generic type in the Dependence class. I would like to know if this is somehow possible to express this using type hints ?

ImogenBits (Imogen) March 13, 2025, 9:06pm 2

This is currently not possible. In general, you’d need higher kinded type variables for this, which we currently don’t have in python. They do get brought up somewhat regularly, but actually making them work would require a lot of effort and they unfortunately also are not super well understood outside of typing circles and don’t really give you that much extra stuff in a language like python. So I wouldn’t hold my breath for them to come out. What you’re already doing is basically the best workaround we have unfortunately.

hauntsaninja (Shantanu Jain) March 14, 2025, 11:09pm 3

You might be able to get something to work using PEP 696, see this example: PEP 696 – Type Defaults for Type Parameters | peps.python.org

Dutcho (Dutcho) March 15, 2025, 7:13am 4

While you cannot go from Provider[int] to int, you can go from int to Provider[int].

Would below work for you?

@dataclass
class Dependence[T]():
    provider: Provider[T]

    def target(self) -> T:
        return self.provider.get()

@dataclass
class Model2:
    dep: Dependence[int]

anentropic (Anentropic) May 20, 2025, 11:09am 5

The lack of this feature makes it cumbersome when trying to compose various generic types together as you either have inference that doesn’t work or you have to copy and paste the type params from the child objects up through all the parents.

The example of TypeScript shows that all these kinds of things are possible, and arguably necessary for very dynamic languages like JS and Python.

jorenham (Joren Hammudoglu) May 20, 2025, 2:42pm 6

This can be done by matching on self in Dependence.target():

from dataclasses import dataclass
from typing import Any

@dataclass
class Provider[T]:
    field: T

    def get(self) -> T:
        return self.field

@dataclass
class Dependence[P: Provider[Any]]:
    provider: P

    def target[T](self: Dependence[Provider[T]]) -> T:
        return self.provider.get()

@dataclass
class Model:
    dep: Dependence[Provider[int]]

m = Model(Dependence(Provider(42)))
reveal_type(m.dep.target())  # int

playground

So no need for HKT :slight_smile:

bryevdv (Bryan Van de Ven) May 21, 2025, 12:48am 7

As long as we are handing out advice… I have been watching this thread since I have a similar situation, but I don’t think can be handled in the same way as above. I’d love to learn there’s some happy, clever Python approach that I just haven’t been able to think up.

In the code below, I’d really like to be able to declare class DocumentEvent[K] or class DocumentEvent[D] instead of having to manually coordinate both together. In TypeScript that would definitely be possible to derive one of those from the other with a type expression.

from enum import Enum
from typing import Literal, TypedDict, TypeVar, TypedDict

class Kind(Enum):
    RootAdded = "RootAdded"
    RootRemoved = "RootRemoved"

class RootAdded(TypedDict):
    kind: Literal["RootAdded"]
    stuff: str

class RootRemoved(TypedDict):
    kind: Literal["RootRemoved"]
    stuff: int

K = TypeVar("K", bound=Kind)
D = TypeVar("D")

class DocumentEvent[K, D]:  # sad-face
    kind: K

    def to_serializable(self) -> D: ...

class RootAddedEvent(DocumentEvent[Kind.RootAdded, RootAdded]):
    ...

I’d be happy to define a property for kind, say, but I’d need to be able to derive K from D.

Tinche (Tin Tvrtković) May 21, 2025, 9:30am 8

I can have a stab at this. We’re missing some constraints here (maybe vis-a-vis serialization?), so until we have those I can suggest the simplest possible version:

from dataclasses import dataclass

@dataclass
class RootAddedEvent:
    stuff: str

@dataclass
class RootRemovedEvent:
    stuff: int

type DocumentEvent = RootAddedEvent | RootRemovedEvent

Jojain (Jojain) May 21, 2025, 11:01am 9

Thanks @jorenham,
So far it seems your solution does actually work for the need I raised. Never thought about using type hints on self that may open more solutions to this kind of problems in the future.

In my typing quest I have also encountered the same issue as @bryevdv and I ended up accepting that the only solution we currently have is the one provided by @Tinche but that is not fullfilling the goal completly.

As far as I know in Python type hinting the only way there is to properly do type mappings is on functions with the overload decorator. I have read some criticism about the verbosity of it on functions and I doesn’t solve the more general type mapping concept we have here.

Some links related to this idea:

bryevdv (Bryan Van de Ven) May 21, 2025, 2:46pm 10

@Tinche The contraints are to have event classes that can report their (enumerated) “kind” and that can produce typed dict representations of themselves for serializing that also include the “kind” in the dict, since the runtime on the other end of the wire will need that in order to re-consituted the model.

Tinche (Tin Tvrtković) May 21, 2025, 2:59pm 11

I figured as much - usually I deal with the wire format in the serialization step. I find modeling the kind thing in the class itself is redundant (the “tag” in the tagged union here, so to speak, is event.__class__). I’ve had a lot of success modeling the serialization format and the pure domain model separately, and it also makes typing simpler. If this appeals to you we can take it to DMs.

bryevdv (Bryan Van de Ven) May 21, 2025, 3:11pm 12

Right, the typed dict is a bit of an “intermediate” format. As always the reason for something like that is “history”. Bokeh is ~15 years old at this point so there’s only so much movement I can make on this part of the codebase without a lot of disruption. This was just an attempt (hope) to tighten up typing where it might be possible in the code that exists, but I think specifying both type parameters here is unavoidable.