Search code examples
pythonpython-3.5mypytypechecking

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?


Solution

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