Home > Enterprise >  How to declare a class variable without a value in a way that suppresses Pylance warning
How to declare a class variable without a value in a way that suppresses Pylance warning

Time:05-26

I love the typechecker in Pylance (VS Code), but there seems to be a situation that I must choose between ignoring a Pylance warning and best practice for class variable declaration. Many times, class variables are initialized using a None type in the class constructor, and the variable is set later. For example:

class Person:
    def __init__(self) -> None:
        self.name:str = None

    def setName(self, name:str) -> None:
        self.name = name

In this case, Pylance gives the following error on the assignment self.name:str = None:

Cannot assign member "name" for type "Person"
Expression of type "None" cannot be assigned to member "name" of class "Person"
Type "None" cannot be assigned to type "str"

Is there any way to declare self.name in the constructor in such a way that a value is not needed and Pylance is happy?

EDIT: Several have suggested the use of typing.Optional to suppress this Pylance warning. However, if another member function is created to return self.name and guarantee that it is returning an instance of str, another Pylance error is generated. See the following example:

class Person:
    def __init__(self) -> None:
        self._name:Optional[str] = None

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, name:str) -> None:
        self._name = name

In this case, Pylance generates the error:

(variable) _name: str | None
Expression of type "str | None" cannot be assigned to return type "str"
Type "str | None" cannot be assigned to type "str"
Type "None" cannot be assigned to type "str"

Ideally, there would be some way to initially "allocate" self._name in the constructor in such a way that its only allowed type is str but is not given any value. Is this possible?

CodePudding user response:

You need to make it optional via typing.Optional:

#!/usr/bin/env python
from typing import Optional


class Person:
    def __init__(self) -> None:
        self.name: Optional[str] = None

    def setName(self, name: str) -> None:
        self.name = name

Python 3.10 supports a simpler syntax via PEP 604's Union type expressions1, e.g.:

    def __init__(self) -> None:
        self.name: str | None = None

CodePudding user response:

Well, if this was certainly a case were the programmer knows better than the type-checker, I think it would be the case for using typing.cast: this is a no-operation in runtime, but which tells the static checkers that the value being cast is actually of that type, regardless of previous declarations:

(to be clear: don't do this:)

import typing as t

class Person:

    def __init__(self) -> None:
        self._name: t.Optional[str, None] = None

    @property
    def name(self) -> str:
        return t.cast(str, self._name)

    ...

However, upon writing this, I came to the realisation that "self._name" might actually be "None" at this time. The typechecker can do some state-checking by following the program steps, but not in such a generic way - trying to read instance.name before it was set should cause in Python a runtime error instead.

So, if the method is called to express exactly that, it works as the tools can follow parts of the code guarded by isinstance (and if one needs a more complicated check than isinstance, take a look on the docs for typing.TypeGuard) - and we thank the tools for ensuring the program will run without a hard to debug error afterwards:

(this is the correct way)


import typing as t

class Person:

    def __init__(self) -> None:
        self._name: t.Optional[str, None] = None

    @property
    def name(self) -> str:
        if isinstance(name:=self._name, str):
            return name
        raise AttributeError()

    @name.setter
    def name(self, name:str) -> None:
        self._name = name
  • Related