For simple cases, you can actually get away with not doing anything in the constructor: both mypy and Pycharm will continue to correctly infer the types of your field if you do this:
class SomeClass:
def called_during_runtime(self) -> None:
self._unassigned_val = 1.0
Of course, you'll need to bear the responsibility of ensuring this function is actually called at runtime: your type checker won't warn you if you do.
If your function is assigning a value to this field in a sufficiently complex way, your type checker may choke and not know what to do. In that case, you can use Variable annotations if you are using Python 3.6 or above:
class SomeClass:
_unassigned_val: float
def called_during_runtime(self) -> None:
self._unassigned_val = 1.0
This is exactly equivalent to the first approach at runtime.
If you need to support older versions of Python, another technique you can do is to create a "bogus" sentinel value which is given a type of Any
, the fully dynamic type:
from typing import Any
BOGUS = object() # type: Any
class SomeClass:
def __init__(self) -> None:
self._unassigned_val = BOGUS # type: float
def called_during_runtime(self) -> None:
self._unassigned_val = 1.0
And if you change your mind and decide you want to skew towards the type checker be more aggressive with its warnings, you can always declare that your value can be either float or None:
from typing import Optional
class SomeClass:
def __init__(self) -> None:
self._unassigned_val = None # type: Optional[float]
def called_during_runtime(self) -> None:
self._unassigned_val = 1.0
def get_with_default(self, default: float) -> float:
if self._unassigned_val is None:
return default
else:
return self._unassigned_val
Note that you can then use a combination of self._unassigned_val is not None
, self._unassigned_val is None
, or isinstance(self._unassigned_val, float)
checks within if statements and asserts to get your type checker to conditionally narrow the type of your field.
This last approach is personally what I do: I'm a fan of type checkers and setting up my tools to be pretty aggressive about detecting potential issues.
One final note regarding ellipsis: using ellipsis as a placeholder is idiomatic only for stubs and when working with things like Protocols method definitions or abstract classes -- basically, in cases where you never end up actually using the value of your field/method parameters/whatever at runtime.