Importing classes from different module levels causes typehint and argument warning disabling

29 views Asked by At

I have this file structure:

 - package
   - sub_package_1
     - __init__.py
     - sub_module_1.py
   - sub_package_2
     - __init__.py
     - sub_module_2.py

In sub_module_1.py we have this code:

class A:
    def __init__(self):
        self.b = B()

    def method_a(self, var: int):
        pass


class B:
    def __init__(self):
        pass

    def method_b(self, var: int):
        pass



class C:  # This class always located in different module, but for example lets place it here
    def __init__(self):
        self.a = A()

    def method_c(self, var: int):
        pass


a = A()
a.b.method_b()  # here we have an argument warning (method_b is waiting for an argument)

c = C()
c.a.b.method_b()  # and also here, the same warning

And in sub_module_2.py we have this code:

from package.sub_package_1.sub_module_1 import A


class C:
    def __init__(self):
        self.a = A()

    def method_c(self, var: int):
        pass


c = C()
c.a.b.method_b()  # but here we don't have a warning

I've leaved some comments in code to show the problem.
When I work in the same module (or even in the same package), I can see all warnings, caused by methods' arguments. But if I move one of the classes outside the package (like I did with C class) I loose all warnings. I still can autocomplete lines, has typehinting etc, but I have no warnings in case I do not pass any argument in c.a.b.method_b().

I feel like this is an import issue, but I can't find the solution.
What is the reason of this behaviour and how can I fix it?

2

There are 2 answers

4
chepner On

As written, the inferred type of c.a is Any, which means you can do anything with it, and mypy (and likely other tools) will be happy. This includes assuming c.a has an attribute b with a 0-argument method method_b.

This is because you haven't provided any annotations for various __init__ functions, which makes mypy essentially ignore them and anything they produce.

Provide explicit return types of None for the __init__ methods, and you should get the expected error even across import boundaries.

class B:
    def __init__(self) -> None:
        pass

    def method_b(self, var: int):
        pass
0
Mika On

Thanks to one wise man for his help, he pointed his finger at the mistake.

The problem (not actually the problem, just IDE optimisation) is that PyCharm has a depth checking limit, which can be configured, but this can cause really heavy lags, so this is not a solution.

The real one is to try not to forget typehint every single line of code. In this current case, we need to do this in sub_module_2.py:

from package.sub_package_1.sub_module_1 import A


class C:
    def __init__(self):
        self.a: A = A()  # Here I forgot to typehint the class
    ...

But much better is to make a typehint at the class level, rather than in the __init__ method, bc on some depth of 'checking-levels' PyCharm stops checking __init__ methods (for the sake of optimization, and that makes sense).

So, if we want to make linter not lose any warning and typehint from nested callings, we need to put the typing in class level, like this:

from package.sub_package_1.sub_module_1 import A


class C:
    """
    So, even if this class call is too deep, 
    PyCharm will check class (as minimum) and spot the `a: A` hint at class level.
    That would not have happened, if hint was in the `__init__` method.
    """

    a: A  # Here is the feature

    def __init__(self):
        self.a = A()
    ...