crosser
crosser

Reputation: 727

Python 3.5 type annotated variable without initial value

I need to declare global variables that have "complex" type and should not be instantiated at import time. In Python 3.6+, I can omit initialization, for example:

log: logging.Logger
pollset: select.poll

I need to make the code compatible with Python 3.5. I can use comment type annotations:

log = ...  # type: logging.Logger
pollset = ...  # type: select.poll

but then I have to provide initial value. This is not a problem at runtime, assigning the initial value of None or ... will do. But any of those trigger mypy typecheck error:

myprog.py:19: error: Incompatible types in assignment (expression has type "ellipsis", variable has type "Logger")

Of course I could use Optional type to allow initializing to None, but then type checking would be weakened. For instance, assigning None value to the variable elsewhere in the code is illegal, but it would not be caught.

Is there an accepted way to use strong type checking of a variable in a way that is compatible with Python 3.5?

Upvotes: 5

Views: 3614

Answers (3)

user7610
user7610

Reputation: 28751

The special treatment of None for PEP 484 was decided in python/typing#61. Sadly, the type checkers (I tried mypy and pyre) don't implement this. The problem is further discussed in python/typing#81.

There are workarounds that can be used

  1. Cast the None, or ellipsis, to the correct dynamic type. This will placate both mypy and pyre.
self.process: subprocess.Popen = cast(subprocess.Popen, None)
self.process: subprocess.Popen = cast(subprocess.Popen, ...)
  1. Suppress the check with a comment, essentially the same effect as before, but nicer to write.
self.process: subprocess.Popen = None  # type: ignore
  1. Declare the variable Optional[...], and then do checks for None on every access
self.process: Optional[subprocess.Popen] = None

Upvotes: 0

MisterMiyagi
MisterMiyagi

Reputation: 50066

According to PEP 484, assigning None is correct.

log = None  # type: logging.Logger

Note that mypy only allows this at class scope. However, you can declare a type and tell mypy to ignore the assignment itself (since mypy 0.700).

log = None  # type: logging.Logger  # type: ignore

Also, you can use a .pyi stub file regardless of Python version.

# my_lib.pyi
log: logging.Logger

However, in non-stub code for versions of Python 3.5 and earlier there is a special case:

from typing import IO

stream = None  # type: IO[str]

Type checkers should not complain about this (despite the value None not matching the given type), nor should they change the inferred type to Optional[...] (despite the rule that does this for annotated arguments with a default value of None). The assumption here is that other code will ensure that the variable is given a value of the proper type, and all uses can assume that the variable has the given type.


Upvotes: 4

Michael0x2a
Michael0x2a

Reputation: 63978

One technique you could do is create a dummy variable with type Any, then use that instead of setting your variables to ... or None. For example:

from typing import Any

_bogus = None     # type: Any
log = _bogus      # type: logging.Logger
pollset = _bogus  # type: select.poll

However, this solution isn't perfect. With variable annotations, we avoided actually creating assigning a value to these variables, so attempting to use log before it's instantiated would result in a NameError at runtime.

However, with this approach, we'd instead get None, which contradicts our declared type.

Maybe this is ok for your use case, but if it isn't, we can get something closer to the variable annotations behavior by sticking these inside of a if TYPE_CHECKING block:

from typing import Any, TYPE_CHECKING

if TYPE_CHECKING:
    _bogus = None     # type: Any
    log = _bogus      # type: logging.Logger
    pollset = _bogus  # type: select.poll

The TYPE_CHECKING variable is always False at runtime, but treated as if it were True by type-checkers like mypy.

(Doing if False also works. It's a common-enough convention that mypy directly supports that as an alternative to using TYPE_CHECKING.)

Upvotes: 1

Related Questions