Skip to content

← Interfaces with Protocols and ABCs step 20 of 26

Medium Framework

Work Out What __all__ and Re-exports Make Public

Two tools decide which of a module's names other code gets, and they use different rules.

  • At runtime, from widgets import * copies names into the importing module. If widgets defines __all__, Python copies exactly the names listed there. Without __all__, it copies every name that does not start with an underscore, including names the module only imported.
  • mypy in strict mode decides which names other modules may import from widgets. It treats an imported name as private unless the module re-exports it. To re-export a name, import it with a same-name alias, such as from decimal import Decimal as Decimal, or list it in __all__.

Take this module:

# widgets.py
import json
from dataclasses import dataclass
from decimal import Decimal as Decimal


@dataclass
class Widget:
    price: Decimal

from widgets import * copies Decimal, Widget, dataclass and json. mypy lets other modules import Decimal and Widget, and rejects from widgets import json with Module "widgets" does not explicitly export attribute "json".

What to write

Write solve, which works out both sets of names from a description of a module, plus the names in __all__ that the module never defines or imports:

def solve(
    defined: list[str],
    imported: list[str],
    reexported: list[str],
    declared_all: list[str] | None,
) -> dict[str, object]: ...
  • defined holds the names the module defines.
  • imported holds the names it imports without re-exporting them.
  • reexported holds the names it imports with a same-name alias.
  • declared_all is the contents of __all__, or None when the module has no __all__. An empty list means __all__ = [], which exports nothing.

The module's bound names are all the names in the first three lists. Return three lists, each in sorted order:

Key Rule
star_exports Without __all__, the bound names that do not start with _, each once. With __all__, the entries of declared_all that are bound.
undefined_in_all Without __all__, []. With __all__, the entries of declared_all that are not bound.
checker_private The entries of imported that are in none of defined, reexported and declared_all. A missing __all__ counts as empty here.

Details the tests check

  • Keep repeated entries when you filter declared_all or imported. If defined holds A and declared_all is ["A", "A"], star_exports is ["A", "A"].
  • A star import copies a name that starts with _ when __all__ lists it. The underscore rule applies only when there is no __all__.
  • A name can appear in more than one input list. A name in defined or reexported never goes in checker_private.
  • Sort with plain sorted. Uppercase letters sort before _, and _ before lowercase letters, so the order is "Path", "_secret", "dataclass".
  • Treat the names as plain strings, and leave the input lists unchanged.
assert solve(["Widget"], ["json", "dataclass"], ["Decimal"], None) == {
    "star_exports": ["Decimal", "Widget", "dataclass", "json"],
    "undefined_in_all": [],
    "checker_private": ["dataclass", "json"],
}
assert solve(["Widget"], ["json", "dataclass"], ["Decimal"], ["Widget", "Wiget"]) == {
    "star_exports": ["Widget"],
    "undefined_in_all": ["Wiget"],
    "checker_private": ["dataclass", "json"],
}

The second case is a real bug that undefined_in_all catches. mypy accepts __all__ = ["Widget", "Wiget"], and from widgets import * then raises AttributeError. Test the imports your package promises, as well as running mypy.