1

I have a class with some fields being Optional. Some uses of that class don't require those fields to be present, but others do. Is there a way to get mypy to diagnose those misuses?

Example:

import attr
from typing import *

@attr.s(auto_attribs=True, frozen=True)
class Point:
    x : int
    y : Optional[int] = None

def get_x(s : Point) -> int:
    return s.x

def get_y(s : Point) -> int:
    assert s.y is not None
    return s.y

def get_y_from_x(x : int) -> int:
    return get_y(Point(x=x)) # <== oops

This program is mypy-clean, despite get_y_from_x passing get_y a Point whose y is None. This will assert at runtime.

Without the assert, mypy correctly complains about the incompatible return type - that get_y returns an Optional[int] when it claims to return an int.

Basically my question boils down to: is there a way for mypy to be able to diagnose the line marked oops above?

I suppose the right way to phrase this question is: get_y currently is annotated to say that s is a Point. But that's not entirely true, it's really a particular kind of Point where s.y is an int. Is that expressible?

Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
Barry
  • 286,269
  • 29
  • 621
  • 977
  • 3
    This is "Mypy-clean" because Mypy knows that `get_y` will *raise an exception* if `s.y` is `None`; the function blows up and never returns anything. Therefore it can narrow the type after the `assert`, and the return type is indeed `int`. This is only an "oops" in that you have asserted something that isn't necessarily true, and you'll get `AssertionError` at runtime; the types are correct and Mypy did the correct thing. By using `assert`in this way, you're basically subverting the type checker. It would be nice though if Mypy could warn when you've made a mistake like this. – shadowtalker Aug 14 '21 at 16:36
  • @shadowtalker Sure, but it's code that unconditionally fails at runtime, which is the kind of the thing I would want to diagnose. Asserting may be the wrong way to do that, which is why I'm asking if there is a right way to do this. – Barry Aug 15 '21 at 13:28
  • This is doable e.g. by defining a ``Point[T]`` where ``T = TypeVar("T", int, None)`` but... it is excessively verbose if you insist on a default ``None`` (since ``None`` is not *any* ``T``) and getting it to work with ``attrs`` might be impossible (or at least unfeasible to preserve ``attrs``' advantage). – MisterMiyagi Aug 15 '21 at 19:08
  • @Barry that's a good point, but I don't think Mypy does that kind of sophisticated analysis (yet). I believe it just checks function signatures and function call sites, and I don't know that it can perform type narrowing across a "function called boundary". There it's probably more precise terminology for this stuff. Anyway, it might be a good feature request on their issue tracker. If nothing else, they will be able to provide a more intelligent explanation of why this isn't done. – shadowtalker Aug 16 '21 at 02:43

0 Answers0