Type hint for a module containing constants

2.1k Views Asked by At

I have a config.py file which has a list of constants, such as:

# config.py
NAME = 'John'
AGE = 23

In another file, I import this file as a module and then pass it as a parameter to other functions. I used ModuleType as the type for this parameter.

import config
from types import ModuleType
def f1(config: ModuleType) -> None:
    print(config.NAME)

The problem is when I run pyright linter, it reports an error:

 79:30 - error: Cannot access member "NAME" for type "ModuleType"
    Member "NAME" is unknown (reportGeneralTypeIssues)

What's the correct way to type hint the config to avoid these errors?

3

There are 3 best solutions below

1
On BEST ANSWER

By far the easiest and most convenient way to handle this is to just not annotate the config argument, or annotate it as Any. You can provide a more specific annotation, but it gets extremely awkward.

The problem with your existing annotation is that your f1 is annotated as taking arbitrary modules as arguments, and arbitrary modules may not have a NAME attribute. (Also ModuleType is in types, not typing.) A correct, specific annotation for f1 would specify that it takes something with a NAME attribute, which you can specify with a custom protocol class:

import typing

class HasName(typing.Protocol):
    NAME: str

def f1(config: HasName) -> None:
    print(config.NAME)

but you'll have to do this for everything you want to define in config, and it'll get even more awkward if you want to allow optional config definitions in config.

Also, if you try to pass config as an argument to f1 now, it still won't work, because when you pass a module as an argument, mypy treats it as just a generic module, and doesn't consider its contents. (I don't know what pyright does, but that's how mypy handles it.) You would have to explicitly cast config:

f1(typing.cast(HasName, config))

which is extremely awkward. Plus, once you have this cast in place, mypy won't report an error even if config doesn't have a NAME attribute, so you've gained no safety at all from all this awkward work.

3
On

As it turns out, all modules are subtypes of types.ModuleType and therefore typing.ModuleType. Thus, what you want is:

def f1(config: config) -> None:
    print(config.NAME)

The first config is just a formal parameter, the second refers to the module by name. The typing module will actually import the members, and it all works.

This does make me wonder what is the point of this. If the module has to be imported in order for the function to be defined, then why pass it as a parameter at all?

0
On

Working as of September, 2023

from typing import Final

UPGRADE : Final[str] = "upgrade"
DOWNGRADE: Final[str] = "downgrade"