4

Question

I want to declare an instance attribute, type hinted as a float, without assigning a value upon initialization. The value is assigned at runtime, after instance initialization (aka after __init__).

According to this answer, I can set the value to None. However, my IDE (PyCharm) raises a PyTypeChecker flag, saying Expected type 'float', got type 'None' instead.

I found if I set the value to be ellipsis (suggested by this answer), aka value = ..., PyCharm no longer complains.

What is the best practice here? Should I use None or ...?


Example Code

class SomeClass:

    def __init__(self):
        """Initialize with an unassigned value."""

        # PyCharm complains here
        self._unassigned_val = None  # type: float

        # PyCharm doesn't complain here
        self._unassigned_val2 = ...  # type: float

    def called_during_runtime(self) -> None:
        """This gets called after __init__ has run."""
        self._unassigned_val = 1.0
        self._unassigned_val2 = 1.0

What it looks like in my IDE:

Sample Code In PyCharm


Version Information

  • PyCharm Community Edition 2019.2.5
  • Python 3.6
Intrastellar Explorer
  • 3,005
  • 9
  • 52
  • 119
  • when does `called_during_runtime` actually get called? – user1558604 Dec 10 '19 at 00:10
  • @user1558604 I just updated question, it's called several minutes into runtime, after the object is initialized – Intrastellar Explorer Dec 10 '19 at 00:14
  • 1
    I think `None` would be the official answer since ellipsis doesn't have an official use. I question whether or not you even need to set those attributes to anything in `__init__`. I think I personally would prefer to get an `AttributeError` if my code accidently asked for that attribute before it was set, rather than getting a random value (either `None` or `...`) that I would then have to figure out how to deal with. – user1558604 Dec 10 '19 at 00:19
  • 1
    Does PyCharm stop complaining if you annotate the variable as `Optional[float]`? (You'll need to import it, `from typing import Optional`) – chris Dec 10 '19 at 00:22
  • @chris the IDE doesn't raise the `PyTypeChecker` warning if I use `Optional`! I guess perhaps doing that leaves the code more flexible, too – Intrastellar Explorer Dec 10 '19 at 00:24
  • 1
    @user1558604 that's a better method I think, thanks for that suggestion as well! – Intrastellar Explorer Dec 10 '19 at 00:25

1 Answers1

2

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.

Michael0x2a
  • 58,192
  • 30
  • 175
  • 224