2025, Dec 30 17:00
Import Where You Use: Clean Python Modules, No Double Imports, and a Safer Public API with __all__
Learn why importing a shared utility in each Python module is correct, avoids double imports, and how to hide it with __all__ to keep a clean public API.
Sharing a utility module across multiple Python modules often triggers concerns about “double imports” and messy namespaces. A typical case looks harmless but raises eyebrows once you notice that an unrelated module accidentally exposes the utility module as an attribute. Let’s unpack what’s really going on and how to keep the codebase clean without overengineering.
The setup
You have three files. One top-level entry point imports both a feature module and a utility module. The feature module also imports the same utility module to call its functions. Here’s the minimal shape of that setup.
app.py:
import toolbox
import section
toolbox.do_something('unique parameters')
section.run_feature()
section.py:
import toolbox
def run_feature():
toolbox.do_something('unique parameters')
From the entry point, it’s technically possible to reach into the feature module and access the utility module through it:
app.py (alternative call you want to avoid):
import section
import toolbox
section.toolbox.do_something()
What’s the actual problem?
The concern has two parts. First, it feels wrong that the feature module appears to “expose” the utility module, making calls like section.toolbox.do_something() possible even though the two modules don’t share a purpose beyond usage. Second, it looks like the utility is being imported “twice,” once by the entry point and once by the feature module.
In practice, the initial structure is fine. The import lines belong in every file that uses the utility functions. That keeps dependencies explicit and local to where they are needed. Also, the utility module is evaluated only the first time it is imported; subsequent imports reference the already loaded module, so there’s no duplicate work being done.
The simplest solution
Do nothing. The original layout is correct and clean. You can call toolbox.do_something('unique parameters') in the entry point and section.run_feature() for the feature-specific work, while section.py keeps its own import toolbox because it needs it. This straightforward rule—import where you use—makes the codebase easier to navigate, test, and reuse from alternate entry points.
Hiding the utility from the feature module’s surface
If it still bothers you that section.toolbox is technically callable, you can declare a narrow public API for the feature module. Define what the module exports explicitly so only the intended names are part of its interface.
import toolbox
def run_feature() -> None:
toolbox.do_something('unique parameters')
__all__ = ["run_feature"]
With this, if you import section, you can still call section.run_feature(), but section.toolbox will no longer be defined and you can’t invoke the utility module through the feature module.
Why this matters
Keeping imports local to where they’re used is a simple, reliable convention. Anyone opening a file can immediately see its external dependencies, which is especially valuable when running unit tests, wiring up alternate entry points, or reading the code in isolation. You avoid hidden couplings, preserve clarity, and keep the module boundaries intentional.
Takeaways
Resist the urge to centralize imports or fight the language’s module system. Import the utility module in every place it’s used—once per file—and rely on Python’s import behavior to handle loading efficiently. If the visibility of a name through another module bothers you, restrict the public API with an explicit __all__ in the feature module. That way the code remains tidy, the intent is obvious, and the surface area you present to callers stays exactly as you want it.