The standard way to solve this is to add an annotation for the attributes on the class level:
from typing import reveal_type
class Toto:
age: int
height: int
def __init__(self, name: str) -> None:
self.name = name
for attr in ['age', 'height']:
setattr(self, attr, 0)
toto = Toto("Toto")
toto.age = 10
reveal_type(toto.name) # reveals str
reveal_type(toto.age) # reveals int
reveal_type(toto.height) # reveals int
Alternatively you can add a block of code under a TYPE_CHECKING condition, which should contain something that a type checker can understand statically -- it is ignored at runtime. So this accomplishes something similar, but is not really needed here:
from typing import TYPE_CHECKING
class Toto:
def __init__(self, name: str) -> None:
self.name = name
for attr in ['age', 'height']:
setattr(self, attr, 0)
if TYPE_CHECKING:
self.age = 0
self.height = 0
Note that in both ways there is no longer a single source-of-truth for the attribute/type definition (compared to e.g. self.name), so a certain (often manageable) effort is required to keep the runtime and static behavior in line.
---
The solution proposed in the accepted answer is really not ideal. It essentially disables type checking on setting attributes, which defeats the purpose of using a type checker in the first place.
For instance, the following code contains a number of (likely) bugs that are now all ignored by the type checker due to __setattr__ accepting an Any :
from typing import Any
class Toto:
def __init__(self, name: str) -> None:
self.name = name
for attr in ['age', 'height']:
setattr(self, attr, 0)
def __setattr__(self, name:str, value: Any):
super().__setattr__(name, value)
toto = Toto("Toto")
# Neither misspelling an attribute name nor assigning a wrong type
# is detected by the type checker.
toto.heigt = 100
toto.age = "this is likely not a valid value for age"