5

Example setup:

from typing import Optional


class A(object):
    def __init__(self):
        self.a: Optional[int] = None
        
    def check_a(self) -> bool:
        return self.a is not None
        
a = A()
if a.check_a():
    print(a.a + 1)  # error: Unsupported operand types for + ("None" and "int")

The check_a method checks what type of variable a is, but mypy does not see this and writes an error. TypeGuard will not help, because it can create a function to check the type, and not a function to check the type of an object variable

Is it possible to somehow make mypy notice this in order to use the function to check the type of the variable self.a without explicitly referring to it in the check? (Use if a.a_check instead of if a.a is not None)?

10
  • You know that the error would never occur, as during runtime that piece of code can only be executed when a.a is not None. However, mypy doesn't take runtime values into account and from the point of view of a compiler a.a could be either an int or None. And None + 1 should generate an error. Commented Oct 7, 2021 at 7:54
  • 1
    typing.cast? Commented Oct 7, 2021 at 7:57
  • 1
    from typing import cast print(cast(int, a.a) + 1) Commented Oct 7, 2021 at 8:15
  • 1
    You do have a "manual" check that type checkers don't understand, so you will also need to make the type checker manually understand your asserted status, which is what cast is for. I don't think there's another way to make the type checker understand a.check_a(); but I could be wrong… Commented Oct 7, 2021 at 8:24
  • 2
    I tried to do it through TypeGuard, but can it be applied here with self? Commented Oct 7, 2021 at 10:03

2 Answers 2

5

There are two ways in which this can be done with the new TypeGuard feature, which can be imported from typing in Python 3.10, and is available from the PyPI typing_extensions package in earlier Python versions. Note that typing_extensions is already a dependency of Mypy, so if you're using Mypy, you likely already have it.

The first option is to change your check_a method to a staticmethod that takes in a variable that might be int or None as an input, and verifies whether or not it is an int. (Apologies, I have changed the names of some of your variables, as I found it quite confusing to have a class A that also had an a attribute.)

from typing import TypeGuard, Optional

class Foo1:
    def __init__(self, bar: Optional[int] = None) -> None:
        self.bar = bar
    
    @staticmethod
    def check_bar(bar: Optional[int]) -> TypeGuard[int]:
        return bar is not None
        

f1 = Foo1()
if f1.check_bar(f1.bar):
    print(f1.bar + 1)

The second option is to use structural subtyping to assert that an instance of class Foo (or class A in your original question) has certain properties at a certain point in time. This requires altering the test method so that it becomes a classmethod, and is a little more complex to set up, but leads to a nicer check once you have it set up.

from typing import TypeGuard, Optional, Protocol, TypeVar

class HasIntBar(Protocol):
    bar: int


F = TypeVar('F', bound='Foo2')


class Foo2:
    def __init__(self, bar: Optional[int] = None) -> None:
        self.bar = bar
    
    @classmethod
    def check_bar(cls: type[F], instance: F) -> TypeGuard[HasIntBar]:
        return instance.bar is not None
        

f2 = Foo2()
if Foo2.check_bar(f2): # could also write this as `if f2.check_bar(f2)`
    print(f2.bar + 1)

You can try both of these options out on Mypy playground here.

Sign up to request clarification or add additional context in comments.

4 Comments

I'm doing it this way now, I'd be wondering if it's possible to do it differently, but apparently it's impossible, thanks
@Lev145 yes, I agree it's imperfect, but I think it's the best that's currently possible :)
Maybe they'll add it later
It seems narrowing down self with TypeGuard is not intended on purpose: “A concern was raised that there may be cases where it is desired to apply the narrowing logic on self and cls. This is an unusual use case, […] It was therefore decided that no special provision would be made for it. If narrowing of self or cls is required, the value can be passed as an explicit argument to a type guard function.” So above solutions may keep the only ones possible.
0

The problem is that, as far as the type checker is concerned, your bool says nothing about the type of your .a. For example, you could have written

class A:
    ...

    def check_a(self) -> bool:
        return True

However, as you probably know, mypy can rule out None within an if x is not None:. Your problem is that you've split your if x is not None: between the function definition and call site, so mypy can't use it to infer the value's not None.

What you can do to fix this is pass the operation you want to do to the A, in which case you'd have something like what's called foreach in other languages. In the context of Optional (and A is a wrapper around a mutable Optional) this can apply a function to values if they exist.

class A:
    ...

    def foreach(self, f: Callable[[int], None]) -> None:
        if self.a is not None:
            f(self.a)

a = A()
a.foreach(lambda x: print(x + 1))

Note I don't have a check_a here.

2 Comments

I understand this, but it does not solve the problem, it will not be convenient to use in the code
@Lev145 you may need to add more information as to why you want to do this. As it is, it's not clear why you don't just use if a.a is not None:. I assume you don't do that because there are parts of your actual problem that don't work well with that that have been lost in making it a minimal example. It would be helpful to know more. Either way, as it stands, this question probably is a duplicate

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.