I have a class Human
which can have a pet and a method to make it talk, either a Dog
or a Cat
. I initialize the Human
with a Dog
, and the method pet_speak
has the correct return type hint Literal["Woof"]
.
If I change the Human
's pet to a Cat
after initialization, the method return type hint isn't updated to Literal["Meow"]
.
Is there a way to change this behavior? This is a simplified version of my problem, creating two classes, such as, HumanDog
and HumanCat
is more complex.
class Cat:
def speak(self): # implicit return type hint Literal["Meow"]
return "Meow"
class Dog:
def speak(self): # implicit return type hint Literal["Woof"]
return "Woof"
# Class `Human` has a `pet`, and can call its `speak()` method
class Human:
pet = Dog()
def pet_speak(self):
return self.pet.speak()
# Helper function to change `human`'s pet
def change_pet(human):
human.pet = Cat()
return human
bob = Human()
woof = bob.pet_speak() # Return type is correctly Literal["Woof"]
bob = change_pet(bob) # change attribute `pet` to `Cat`
meow = bob.pet_speak() # Return type is Literal["Woof"], but should be Literal["Meow"]
The closest question I could find was Can you type hint overload a return type for a method based on an argument passed into the constructor? , but it also adds an overhead to every Human
method and subclasses.
Sorry if I dumbed the problem down too much, here it's more similar to what I'm trying to do:
from __future__ import annotations
class Api:
def request(self):
return "Response"
class AsyncApi:
async def request(self):
return "Response"
class Database:
api: Api | AsyncApi = Api()
def get(self):
return self.api.request()
def Async(database):
database.api = AsyncApi()
return database
async def main():
db = Database()
db.get()
adb = Async(db)
await adb.get() # typing error
If you add the missing annotation run the code through Pyright (or Mypy), it will spot an error here:
def change_pet(human: Human) -> None:
human.pet = Cat()
# ^
# Cannot assign member "pet" for type "Human"
# Expression of type "Cat" cannot be assigned to member "pet" of class "Human"
# "Cat" is incompatible with "Dog"
return human
Indeed, the type of human.pet
is Cat
. Generally, the type of an object can't change when you're mutating it. This makes sense, for example because an object might be shared:
class CatHumans:
def __init__(self, humans: Iterable[Human]) -> None:
# somehow establish that all the `humans` have a cat
self._humans = list(humans)
def do_something_assuming_humans_have_cats(self) -> None:
for human in self._humans:
assert human.pet.speak() == "Meow"
...
alice, bob, charlie = make_humans_with_cats(3)
cat_humans = CatHumans([alice, bob, charlie])
alice.pet = Dog()
cat_humans.do_something_assuming_humans_have_cats()
It would be very difficult if not impossible for type checkers to track situations like this.
I don't know what real problem you're solving, but I have two solutions:
pet
:A common protocol/base class:
class Human:
pet: Animal = Dog()
Or a union type:
from typing import Union
class Human:
pet: Union[Cat, Dog] = Dog()
from typing import Generic, TypeVar
Pet = TypeVar("Pet", bound=Animal)
class Human(Generic[Pet]):
def __init__(self, pet: Pet) -> None:
self.pet = pet
This still doesn't let you have the same object be Human[Dog]
today and Human[Cat]
tomorrow, but you can have distinct Human[Cat]
and Human[Dog]
objects.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.