typing for rare case fallback None value

35 views Asked by At

Trying to avoid typing issues I often run into the same problem.

E.g. I have a function x that very rarily returns value None, all other times it returns int.


def x(i: int) -> Union[int, None]:
    if i == 0:
        return
    return i

def test(i: int):
    a = x(i)
    # typing issue: *= not supported for types int | None and int
    a *= 25

x used very often in the codebase and most of the time i was already checked a hundred times that x(i) will indeed return int and not None. Using it as int right away creates typing warnings - e.g. you can't multiply possible None value.

What's best practice for that case?

Ideas I considered:

  1. There is no real sense to check it for None with if a is None: return as it's already known.
  2. a *= 25 # type: ignore will make a an Unknown type.
  3. a = x(i) # type: int will make the warning go away. But will create a new warning "int | None cannot be assigned to int"
  4. a = cast(int, x(i)), haven't tested it much yet.

I usually end up changing return type of x to just int, adding ignore in return # type: ignore and mention in the docstring that it can return None, it helps avoiding contaminating the entire codebase with type warnings. Is this the best approach?

def x(i: int) -> int:
    """might also return `None`"""
    if i == 0:
        return # type: ignore
    return i
1

There are 1 answers

3
chepner On BEST ANSWER

This might be a case where an exception is better than a return statement you never expect to be reached.

def x(i: int) -> int:
    if i == 0:
        raise ValueError("didn't expect i==0")
    return i

def test(i: int):
    try:
        a = x(i)
    except ValueError:
        pass

    a *= 25

Code that is confident it has sufficiently validated the argument to x can omit the try statement.

Statically speaking, this is accurate: if x returns, it is guaranteed to return an int. (Whether it will return is another question.)


Ideally, you could define a refinement type like NonZeroInt, and turn i == 0 into a type error, rather than a value error.

# Made-up special form RefinementType obeys
#
#  isinstance(x, RefinementType[T, p]) == isinstance(x, T) and p(x)
NonZeroInt = RefinementType[int, lambda x: x != 0]

def x(i: NonZeroInt) -> int:
    return i

x(0)  # error: Argument 1 to "x" has incompatible type "int"; expected "NonZeroInt"  [arg-type]

i: int = 0
x(i)  # same error

j: NonZeroInt = 0  #  error: Incompatible types in assignment (expression has type "int", variable has type "NonZeroInt")  [assignment]

x(j)  # OK

k: NonZeroInt = 3  # OK
x(k)  # OK