30

I'm doing some experiments with typing in Python 3.6 and mypy. I want to design an entity class that can be instantiated in two ways:

  • By the use of an ordinary initializer (p = Person(name='Hannes', age=27))
  • Statically from a state object (p = Person.from_state(person_state)).

The Entity class, from which Person derives, has the state class as a generic parameter. However, when validating the code with mypy, I receive an error that Person.from_state doesn't pick up the state type from the class it inherits from:

untitled2.py:47: error: Argument 1 to "from_state" of "Entity" has incompatible type "UserState"; expected "StateType"

I thought that by inheriting from Entity[UserState], StateType would be bound to UserState and the method signatures in the child classes would update accordingly.

This is the full code. I have marked the line where I suspect I'm doing things wrong with ?????. Line 47 is almost at the bottom and marked in the code.

from typing import TypeVar, Generic, NamedTuple, List, NewType

EntityId = NewType('EntityId', str)

StateType = TypeVar('StateType')

class Entity(Generic[StateType]):
    id: EntityId = None
    state: StateType = None

    @classmethod
    def from_state(cls, state: StateType): # ?????
        ret = object.__new__(cls)
        ret.id = None
        ret.state = state
        return ret

    def assign_id(self, id: EntityId) -> None:
        self.id = id

class UserState(NamedTuple):
    name: str
    age: int

class User(Entity[UserState]):
    def __init__(self, name, age) -> None:
        super().__init__()
        self.state = UserState(name=name, age=age)

    @property
    def name(self) -> str:
        return self.state.name

    @property
    def age(self) -> int:
        return self.state.age

    def have_birthday(self) -> None:
        new_age = self.state.age+1
        self.state = self.state._replace(age=new_age)

# Create first object with constructor
u1 = User(name='Anders', age=47)

# Create second object from state
user_state = UserState(name='Hannes', age=27)
u2 = User.from_state(user_state) # Line 47

print(u1.state)
print(u2.state)
Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
Hannes Petri
  • 864
  • 1
  • 7
  • 15
  • 1
    Is this some academic research, or you're solving a real problem? Asking just because data model looks slightly weird – Slam Feb 06 '18 at 10:47
  • It's not a "real problem" in the sense of production system. I'm trying to see if it's feasible to use this methodology in python: https://vaughnvernon.co/?p=879. However, as for the generics, I genuinely wonder how it works and what I'm doing wrong. – Hannes Petri Feb 06 '18 at 10:53
  • 3
    Seems related: https://github.com/python/mypy/issues/1337 – de1 Oct 07 '18 at 17:37
  • 1
    Interestingly this error isn't reproducible with Python 3.6.10 or 3.8.6 and mypy 0.790. @HannesPetri which version of mypy are you using? – Jack Smith Jan 15 '21 at 11:57
  • 2
    python 3.9 gives me `UserState(name='Anders', age=47) UserState(name='Hannes', age=27)` – Marcello Romani Feb 19 '21 at 18:44
  • @MarcelloRomani for me too, using CPython 3.9.1 – Levente Simofi May 07 '21 at 18:42

2 Answers2

2

This was a bug in mypy that was fixed in mypy 0.700. As several people in the comments noted, that line of code validates fine in newer versions.

Note that in newer versions of mypy, the code in the question has a different problem:

main.py:8: error: Incompatible types in assignment (expression has type "None", variable has type "EntityId")
main.py:9: error: Incompatible types in assignment (expression has type "None", variable has type "StateType")

But that's outside the scope of the question and up to you to resolve however you'd like.

Angus L'Herrou
  • 429
  • 3
  • 11
-3

I know this question is a little old, but just for future reference:

from __future__ import annotations
from typing import NamedTuple, Optional, Type


class UserState(NamedTuple):
    name: str
    age: int


class Entity:
    id: Optional[str]
    state: UserState

    @classmethod
    def from_state(cls: Type[Entity], state: UserState) -> Entity:
        entity_from_state: Entity = object.__new__(cls)
        entity_from_state.id = None
        entity_from_state.state = state
        return entity_from_state

    def assign_id(self, id: str) -> None:
        self.id = id


class User(Entity):
    def __init__(self, name: str, age: int) -> None:
        self.state = UserState(name=name, age=age)

    @property
    def name(self) -> str:
        return self.state.name

    @property
    def age(self) -> int:
        return self.state.age

    def have_birthday(self) -> None:
        new_age = self.state.age + 1
        self.state = self.state._replace(age=new_age)


# Create first object with constructor
u1 = User(name="Anders", age=47)

# Create second object from state
user_state = UserState(name="Hannes", age=27)
u2 = User.from_state(user_state)  # Line 47

print(u1.state)
print(u2.state)
Gijs Wobben
  • 1,974
  • 1
  • 10
  • 13