The AttributeError is caused by the as in the import statement in file c.py.
The whole process is like:
main.py created module a, added it to sys.modules and initialized it;
main.py created module a.b, added it to sys.modules and began to execute its code;
b/__init__.py (a and a.b are already in sys.modules) created module a.b.c, added it to sys.modules and begin to execute its code;
b/c.py created module a.b.d, added it to sys.modules, executed its code, added it as an attribute 'd' of module a.b, then tried but failed to bind a.b.d to name d. The problem is that module a.b was not done initializing yet, so the attribute 'b' was not in module a.
WHY
To understand this, you should know that an import statement does two things (in both Python 2 and Python 3).
- Find a module or modules, and initialize it or them if necessary;
- Define a name in the local namespace and bind it to a certain module.
Module Finding
The former one calls the __import__ hook, which loads the module and initializes it. In Python 2 the hook is imputil.ImportManager._import_hook by default, and it works like this.
- Check if the module is in
sys.modules;
- If not, find the module and get its code;
- Create the module and add it to
sys.modules;
- Run the code within the module's namespace;
- Return the module.
If the statement is like import a.b.c, the module finding process will recursively find module a, a.b, a.b.c, and keep track of them in sys.modules. If module a.b is returned, it is set as an attribute 'b' of module a. Finally, the module finding process will return the top module a.
If the statement is like from a.b import c,d, the outcome is a little different. In this case the bottom module (i.e. module a.b) will be returned.
Name Binding
If you use an import [module] statement, the name of the top module will be bound to the return value (which is the top module).
If you use an import [module] as [name] statement, then [name] will be bound to the bottom module (by accessing the attributes of the top module).
If you use an from [module] import [identifier], then the name of the bottom module will be bind to the return value (which in from import statement is the bottom module).
Example
import a.b.c # a <- <module 'a'>
import a.b.c as c # c <- <module 'a'>.b.c
from a.b import c # c <- <module 'a.b'>.c
In your question, the import statement in c.py occurs when module a.b is half initialized and is not yet registered in module a's attributes. So import as will encounter a problem when binding a.b.c to c. However, since module a.b is already registered in sys.modules, using from import will not encounter such a problem.