5

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?

4
  • Perhaps just the comment? Commented Jun 13, 2019 at 16:06
  • Python doesn't have variable declarations, only variable definitions. What do you really "need" to do? Commented Jun 13, 2019 at 17:41
  • After log: logging.Logger, there is still no variable named log. Commented Jun 13, 2019 at 17:43
  • @chepner A variable annotation is enough to create said variable if that is meaningful for its scope. In a local scope, it creates an unbound local variable. "However, annotating a local variable will cause the interpreter to always make it a local:". python.org/dev/peps/pep-0526/… Commented Jun 14, 2019 at 9:20

3 Answers 3

4

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.


Sign up to request clarification or add additional context in comments.

4 Comments

All this should work according to the PEP, but unfortunately mypy (0.670) does not honour the exception and complains, both if I assign None and if I add the # type: ignore.
@crosser I've verified it with mypy 0.701. You can test it directly with mypy -c 'foo = None # type: int # type: ignore' from the terminal.
@crosser You need at least mypy 0.700 for this. See the updated answer.
I understand, thank you for the clarification. I will still go with the suggestion from the other answer for the sake of wider compatibility.
1

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.)

Comments

0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.