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