Skip to content

← Structural Typing and the Hard Parts step 22 of 24

Medium Framework

__all__, re-export rules, and the accidental public API

Without a declared surface, every module is public by accident. A user discovers yourpkg.internal.helpers.frobnicate through autocomplete, imports it, and it is load-bearing forever. Worse, an __init__.py that does from .db import session has just re-published session — and from yourpkg import * will hand it out.

Three separate surfaces, and they do not agree

Runtime import *. With __all__ present, exactly those names — and AttributeError at import time for any entry with no binding. Without __all__, every bound name not starting with _, including everything the module merely imported.

The type checker. --no-implicit-reexport (part of --strict) says “imported symbols are considered private by default”. A name becomes re-exported to the checker in exactly three ways:

  • import X as X
  • from Y import X as X
  • from Y import * where Y defines __all__

Listing the name in __all__ also re-exports it. The redundant-looking as X is the deliberate, spec-sanctioned marker for “yes, I meant to publish this”.

__all__ itself. Here is the verified divergence: mypy --strict does not validate __all__ contents. __all__ = ["Widget", "Typo"] with no Typo anywhere passes cleanly. Pyright’s reportUnsupportedDunderAll catches it. If mypy is your only checker, a typo’d __all__ is an AttributeError at import for the first user who writes from pkg import * — and your test suite, which imports by name, never sees it.

Your task

Model the module namespace as data and compute all three surfaces.

def solve(
    defined: list[str],       # bound by def / class / assignment
    imported: list[str],      # bound by plain `from Y import Z`
    reexported: list[str],    # bound by `from Y import Z as Z`
    declared_all: list[str] | None,   # None means no __all__ at all
) -> dict[str, object]:

Return:

key value
"star_exports" sorted names from pkg import * actually binds
"undefined_in_all" sorted names in __all__ with no binding
"checker_private" sorted bound names --no-implicit-reexport treats as private

With __all__ present, star_exports is the listed names that are bound (the rest land in undefined_in_all). With __all__ absent, it is every bound name not starting with _ — note that this includes imports, which is the leak.

A name is private to the checker iff it was imported plainly and is neither re-exported with as nor listed in __all__. Names you defined are always public to the checker.

Watch the sort: Python sorts by code point, so "Path" precedes "_secret" precedes "dataclass".

The habit this encodes

Declare __all__ in every __init__.py, keep it accurate, and use from .thing import Thing as Thing for deliberate re-exports. That one convention gives you a surface a reviewer can read, a checker that agrees with the runtime, and a deprecation story that does not start with “someone imported our internals”.

Loading visualization…