Search code examples
pythonpython-typingmypy

How to prevent type alias defined in a stub file from being used in other modules?


I'm working on a Python 3.13.1 project using mypy 1.14.0 for static type checking.
I have a module named module.py with a function function that returns a type with a very long name,
Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again.
To make the code more readable, I've defined a type alias T in the corresponding stub file module.pyi .

Here's a simplified version of my code module.pyi:

T = Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again

def function()->T: pass

class Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again: pass

I want to prevent following illegal_usage_of_T.py from using the T type alias.

import module

foo:module.T = module.function()

Ideally, when I run mypy illegal_usage_of_T.py, I'd like to get an error message indicating that the type T is undefined.

What I tried

Google search

I've google searched for "mypy type alias only used in stub file" but couldn't find a solution that prevents mypy from recognizing the type alias in other modules. I expected that defining T only in the stub file would limit its scope, but it seems that mypy is able to find the type alias even in other modules.

fixing code

I've tried several approaches, including:

  • Renaming the type alias: I changed T to _T to make it less likely to be found by other modules, but this didn't resolve the issue.
  • Using if TYPE_CHECKING: I tried conditionally defining the type alias within an if TYPE_CHECKING block, but this also didn't prevent the type alias from being used in other modules.
  • Limiting exports: I added __all__ = ["function", "Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again"] to the module.pyi file to explicitly control what names are exported, but the type alias T was still accessible.

Here's the modified code for module.pyi:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    _T = Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again

def function()->_T: pass

class Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again: pass

__all__ = ["function", "Type_whose_name_is_so_long_that_we_do_not_want_to_call_it_over_and_over_again"]

And here's the illegal_usage_of_T.py file:

import module

foo:module._T = module.function()  # Still works

I expected that at least one of these approaches would prevent illegal_usage_of_T.py from accessing the _T type alias, but none of them worked.


Solution

  • Apart from writing your own mypy plugin, there isn't really a way to do this.

    Prefixing items with an underscore is by far the overwhelmingly adopted convention to indicate names which aren't supposed to be exported (used outside of the module it is defined in); you can see this convention adopted in Python's own typeshed project. Some IDEs (like PyCharm or VSCode with pyright) in fact do show errors if you try to access underscore-prefixed items from a module, but this isn't part of mypy.

    Apart from just using pyright instead of mypy, the closest thing that exists is Ruff's import-private-name rule, but this doesn't activate unless you use the name in a runtime context (type annotations, like foo: module.T, don't count, and won't trigger the linting).

    As for the others:

    • if TYPE_CHECKING - this has no effect in .pyi stub files.
    • __all__ - this only has effect for star imports (from module import *) and names which would otherwise not be re-exported. Direct access to module attributes (like module.T or from module import T) is never prevented due to the lack of a name ("T") in __all__, if T was defined inside module.