I made this simple function which I want to check with mypy and pylint. It just parses a string and converts it to the appropriate type.
import re
from typing import Any, Callable
def parse_constant(constant: str) -> Any:
for reg, get_val in [
(re.compile(r'\'(.*)\''), str),
(re.compile(r'true', re.IGNORECASE), lambda _: True),
(re.compile(r'false', re.IGNORECASE), lambda _: False),
(re.compile(r'([0-9]+)'), int),
(re.compile(r'([0-9]+\.[0-9]+)'), float)
]:
match = reg.fullmatch(constant)
if match is not None:
if len(match.groups()) == 0:
val = None
else:
val = match.groups()[0]
return get_val(val)
return None
It works fine but mypy complains: I get error: "object" not callable
at line 18 (return get_val(val)
).
Now if I replace, str
by lambda x: str(x)
mypy is happy but pylint
complains with Lambda may not be necessary
.
What is the proper way to fix that?
The issue is that MyPy must infer get_val
from a mixture of Callable
and Type
. In this case, MyPy selects the base instead of the union of the types. Explicitly annotate the types to avoid too broad inference.
Inside the for
loop, only the loop variables can be annotated. By moving the iterable outside the loop, it can be annotated:
import re
from typing import Any, Callable, Pattern, List, Tuple
cases: List[Tuple[Pattern[str], Callable]] = [
(re.compile(r'\'(.*)\''), str),
(re.compile(r'true', re.IGNORECASE), lambda _: True),
(re.compile(r'false', re.IGNORECASE), lambda _: False),
(re.compile(r'([0-9]+)'), int),
(re.compile(r'([0-9]+\.[0-9]+)'), float)
]
def parse_constant(constant: str) -> Any:
for reg, get_val in cases:
match = reg.fullmatch(constant)
if match is not None:
if len(match.groups()) == 0:
val = None
else:
val = match.groups()[0]
return get_val(val)
return None
Moving the cases outside of the function has the added advantage that they are created only once. This is especially of importance for re.compile
, which is now compiled once and then stored for repeated use.