Образно работает так:
Выполняются затронутые модули - все __init__.py
по пути и сам модуль выполняется. Выполнение кода в файле модуля происходит сверху вниз - и рождает классы, функции, производит импорты и т.д. В результате выполнения у каждого выполненного модуля заполняется пространство имен.
Происходит импорт чего-то из этого пространства имен модуля-источника в пространство имен вашего модуля под каким-то именем. То есть "что-то из чужого пространство имен будет доступна мне под таким вот именем". Хоть вообще всё чужое пространство имен.
В других языках вида пхп можно сделать include и глобальное пространство имен замусорится новыми элементами и их можно вызывать. В python пространство имен каждого модуля изолировано и нужно явно его заполнить для последующей работы с элементами.
Поэтому сначала нужно положить это в наше пространство имен найдя его в чужом пространстве имен, или само чужое пространство имен сделать доступным под нужным именем. Это и делает импорт. После этого мы можем работать с этим под нужным именем.
И не имеет значения откуда элемент появился в чужом пространстве имен. Для импорта это просто "вот это у тебя с этим именем вот сюда мне под этим именем сделай доступным"
(поэтому многочисленные from typing import ...
в чужом модуле замусоривают подсказку IDE - такова уж специфика)
from pprint import pprint
поместил функцию pprint
в пространство имен модуля A (под этим же именем pprint
)
ваш import A
просто делает доступным пространство имен этого модуля через A и вы можете вызвать A.pprint(...
Но сам pprint
не попадает автоматом в пространство имен модуля B. Вы должны явно задавать что из чужого модуля вы хотите сделать видимым у себя и под каким именем.
Исключение вот такой импорт, который все же явный
from A import *