21

I've got the following function, which given a string of the form 'a-02/b-03/foobarbaz_c-04', will extract the digits after a, b and c. The issue is that, for my use case, the input strings may not contain c, such that there will be no digits to extract.

Here's the code:

from typing import Tuple, Optional


def regex_a_b_c(name: str) -> Tuple[int, int, Optional[int]]:
        a_b_info = re.search('a-(\d\d)/b-(\d\d)/', name)
        a, b = [int(a_b_info.group(x)) for x in range(1, 3)]
        c_info = re.search('c-(\d\d)', name)
        if c_info:
            c = int(c_info.group(1))
        else:
            c = None   
        return a, b, c

The issue I have is that, despite trying to make it clear that the last return argument is an Optional[int], I can't get my linter to stop complaining about the variable c.

I get a warning at the line c = None that says:

Incompatible types in assignment (expression has type None, variable has type int)

How can I solve the issue?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Daniel
  • 11,332
  • 9
  • 44
  • 72

3 Answers3

35

If you don't annotate a variable, mypy will infer its type based on the very first assignment it sees.

So in this case, the line c = int(_info.group(1)) appears first, so mypy decides that the type must be int. It then subsequently complains when it sees c = None.

One way of working around this limitation is to just forward-declare the variable with the expected type. If you are using Python 3.6+ and can use variable annotations, you can do so like this:

c: Optional[int]
if c_info:
    c = int(c_info.group(1))
else:
    c = None

Or perhaps more concisely, like this:

c: Optional[int] = None
if c_info:
    c = int(c_info.group(1))

If you need to support older versions of Python, you can annotate the type using the comment-based syntax, like so:

c = None  # type: Optional[int]
if c_info:
    c = int(c_info.group(1))

rje's suggestion of doing:

if c_info:
    c = int(c_info.group(1))
    return a, b, c
else:
    return a, b, None

...is also a reasonable one.

user2357112
  • 260,549
  • 28
  • 431
  • 505
Michael0x2a
  • 58,192
  • 30
  • 175
  • 224
6

Aside from the nice approach given by this answer, I came across another way to get mypy to ignore the line by adding comment like the following:

c = None    # type: ignore

This seems to ignore the type for the current line, but does not effect the type inference for the other areas where the variable is used.

alper
  • 2,919
  • 9
  • 53
  • 102
krumpelstiltskin
  • 486
  • 7
  • 17
  • 7
    Don’t know why this was downvoted, it’s the best answer in my opinion: because mypy is a type annotation program, it’s not supposed to cause you to restructure your code (which all the other answers do). Thanks :-) – Louis Maddox Jul 09 '21 at 22:31
  • I'm downvoting because this *breaks* type inference - mypy will treat `c` as having type `int` instead of type `Optional[int]`, and will treat all `int` operations as valid on `c`, even though `c` could be `None`. – user2357112 Nov 02 '21 at 16:59
  • I agree with Louis here, for now this is the best solution. This honestly seems like a shortcoming of MyPy. Just because it is incapable of updating its inferred type, doesn't mean that we should refactor to something less readable/intuitive. Thank you! – Zach Nov 03 '21 at 08:46
2

You should return either a tuple a,b,c or a tuple a,b without including c. This way you do not need to assign a value of None to c at all.

if c_info:
    c = int(c_info.group(1))
    return a, b, c
else:
    return a, b
rje
  • 6,388
  • 1
  • 21
  • 40
  • But that's the reason why I defined the return type to have an `Optional[int]`. Is there really no way around it? – Daniel Oct 15 '18 at 18:53
  • Thanks for the update. That's unfortunately suboptimal in that it doesn't allow me to easily unpack the results when I use the function. That's why I wanted a tuple of int, int, Optional[int]. – Daniel Oct 15 '18 at 18:57
  • Hmm. Did you try declaring c as an optional int, e.g. `c: Optional[int] = None`? – rje Oct 15 '18 at 19:01