7

Give something as follows:

import importlib

module_path = "mod"
mod = importlib.import_module(module_path, package=None)
print(mod.Foo.Bar.x)

where mod.py is:

class Foo:
    class Bar:
        x = 1

mypy file.py --strict raises the following error:

file.py:7: error: Module has no attribute "Foo"  [attr-defined]

I'm wondering how one is supposed to go about type-hinting this, or if this is something which would typically just be ignored with # type: ignore[attr-defined] (assuming that the code is necessary, and the only options are type-hinting or ignoring the type-hint)?


Why I am using importlib in this situation

The way that importlib is being used is that there's some path:

x.y.<changes>.z

Where <changes> is dynamic, but the others are fixed. I'm confident that the module will contain the attributes which are being called, but due to <changes>, importlib is used for the import.

Which might be summarised as:

I do not know precisely which module I will be importing, but I know it will have a class Foo in it.

4
  • Can I ask why you're using importlib in this situation? Is it a case of "I do not know which module I will be importing, and which classes it will have"? Is it a case of "I do not know precisely which module I will be importing, but I know it will have a class Foo in it"? Is it a case of "I know exactly which module I will be importing, I just don't know where it will be found"? Or is it something else entirely? Commented Sep 7, 2021 at 17:02
  • 2
    importlib can import arbitrary code – a type checker has no way of knowing what module it provides. The only option is to structurally type the entire module and hint mod with it, which seems to defeat the point of using importlib. Commented Sep 7, 2021 at 17:16
  • 1
    @AlexWaygood I've edited the post Commented Sep 7, 2021 at 18:16
  • @MisterMiyagi I'm not too sure what it means here to "structurally type the entire module". Commented Sep 7, 2021 at 18:17

1 Answer 1

4

As was alluded to by @MisterMiyagi in the comments, I think the solution here is to use structural, rather than nominal, subtyping. Nominal subtyping is where we use direct class inheritance to define type relationships. For example, collections.Counter is a subtype of dict because it directly inherits from dict. Structural subtyping, however, is where we define types based on certain properties a class has or certain behaviours it displays. int is a subtype of typing.SupportsFloat not because it directly inherits from SupportsFloat (it doesn't), but because SupportsFloat is defined as a certain interface, and int satisfies that interface.

When type-hinting, we can define structural types using typing.Protocol. You could satisfy MyPy in this situation like this:

import importlib
from typing import cast, Protocol

class BarProto(Protocol):
    x: int
    
class FooProto(Protocol):
    Bar: type[BarProto]
    
class ModProto(Protocol):
    Foo: type[FooProto]

module_path = "mod"
mod = cast(ModProto, importlib.import_module(module_path, package=None))

print(mod.Foo.Bar.x)

reveal_type(mod)
reveal_type(mod.Foo)
reveal_type(mod.Foo.Bar)
reveal_type(mod.Foo.Bar.x)

We've defined several interfaces here:

  • BarProto: in order to satisfy this interface, a type has to have an attribute x that's of type int.
  • FooProto: in order to satisfy this interface, a type has to have an attribute Bar that is a class of which instances satisfy the BarProto protocol.
  • ModProto: in order to satisfy this interface, a type has to have an attribute Foo that is a class of which instances satisfy the FooProto protocol.

Then, when importing the module, we use typing.cast to assert to the type-checker that the module we're importing satisfies the ModProto protocol.

Run it through MyPy, and it informs us it has inferred the following types:

main.py:18: note: Revealed type is "__main__.ModProto"  
main.py:19: note: Revealed type is "Type[__main__.FooProto]"
main.py:20: note: Revealed type is "Type[__main__.BarProto]"
main.py:21: note: Revealed type is "builtins.int"

Read more about structural subtyping in python here and here.

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

Comments

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.