I use neovim and use the pyright linter through mason-lspconfig in order to statically type check my code. I write many type hints; here's an example:
def example(user_input: str) -> None:
tokens: List[Token] = []
position = 0
length_of_input: int = len(user_input)
while position < length_of_input:
match: Union[re.Match[str], None] = None
sliced_input = user_input[position:]
# There's more code but you probably get the point
I have a series of questions regarding the best practices with type hinting:
Here's where I get to the part about satisfying the linter. Here's what my code initially looked like:
# There's a lot of context that goes with this but just assume factor_node_result.tokens is a List
tokens = factor_node_result.tokens.copy()
The linter tells me that
None has no method copy()
I changed it to this so that it stops complaining; however, both versions of my code work. The linter just stops complaining when I use this version:
if factor_node_result.tokens is not None:
tokens = factor_node_result.tokens.copy()
Thing is, if you have a good linter and you write decent code, whatever the linter complains about is a potential issue with your code.
"But it works without the type" does not change that. For one, Python always works without the type hints, that's not why it's there. Secondly, unless you know absolutely sure that it will work for any possible value, you can't really say it works. And even then, the fact that it works now, doesn't mean it will still work in the future, when you call a function from somewhere else, or perhaps run the script on a new version of Python, with different dependencies, etc.
As for checking for None
, yes you should in some cases, if the function can reasonably return None
(or exit without explicitly returning anything, which is the same). For example:
from typing import Optional
def question(answer: bool) -> Optional[int]:
if answer:
return 42
wisdom = question(False)
if wisdom is None:
print("You're not worthy!")
else:
print(f"Your answer is {wisdom}")
However, a function that always returns None
(i.e. that solely relies on side effects, or is just a method operating on an object) shouldn't need the hint. After all, you wouldn't be calling methods on None
.
All four questions:
None
, should you still specify it like I do with the example function?str
instead of AnyStr
.AnyStr
includes both bytes
and str
. No, it's not bad practice to use str
, if your code doesn't support bytes
, you should use str
. (keep in mind that it has to be either str
or bytes
for all instances of use in a single signature)Finally, here's an example where a linter will typically fail, and where specifying -> None
helps:
def fn(x):
def always_true():
return True
if always_true():
return
return x
def stripped_lower(x: str):
return x.lower().strip()
stripped_lower(fn('test'))
This code will look fine to many linter and analysis tools. However, to you and me it should be clear that the function fn()
can only ever return None
, due to what always_true()
returns. However, to a linter, True
is just a runtime bool
value, and it won't use that in its analysis, since a bool
might be False
as well. So, from its point of view, fn('test')
may well return a string. stripped_lower()
is clear about expecting a string, but will fail with an exception if passed None
- which it always will.
If you change the signature to def fn(x) -> None:
, your tools will pick up on the problem and correctly show a type warning.
Note that I said "a function that always returns None
shouldn't need the hint" - that's true here as well. Although providing the hint helps here, this function is poorly written and rewriting it so that it doesn't need the hint would be better than adding the type hint. However, sometimes you don't have that luxury (perhaps you don't control the problematic parts of the code) and then adding the hint may be a better option.