Search code examples
python-3.xpylintmypy

How to fix mypy inferring loop variable as "object"?


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?


Solution

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