some python devs may think import and import as statements behave exactly the same, except that import as binds the imported module to a different name; but this is not very true (in python 3.6);

assuming package layout:

sound/                          Top-level package
      __init__.py               Initialize the sound package
      formats/                  Subpackage for file format conversions
              __init__.py
              wavread.py
              wavwrite.py
              aiffread.py
              aiffwrite.py
              auread.py
              auwrite.py
              ...
      effects/                  Subpackage for sound effects
              __init__.py
              echo.py
              surround.py
              reverse.py
              ...
      filters/                  Subpackage for filters
              __init__.py
              equalizer.py
              vocoder.py
              karaoke.py
              ...

here is the test case:

import sound.effects
del sound.effects
#import sound.effects.echo                   # pass
#import sound.effects.echo as echo2          # fail
#from sound.effects import echo              # pass
#from sound.effects import echo as echo2     # pass

you will find import will pass but import as will fail; this is an implementation detail: import as generates additional bytecodes: LOAD_ATTR;

lets dis these statements in python 3.6:

>>> dis.dis('import sound.effects.echo')
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sound.effects.echo)
              6 STORE_NAME               1 (sound)
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE
>>> dis.dis('import sound.effects.echo as echo2')
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sound.effects.echo)
              6 LOAD_ATTR                1 (effects)
              8 LOAD_ATTR                2 (echo)
             10 STORE_NAME               3 (echo2)
             12 LOAD_CONST               1 (None)
             14 RETURN_VALUE
>>> dis.dis('from sound.effects import echo')
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (('echo',))
              4 IMPORT_NAME              0 (sound.effects)
              6 IMPORT_FROM              1 (echo)
              8 STORE_NAME               1 (echo)
             10 POP_TOP
             12 LOAD_CONST               2 (None)
             14 RETURN_VALUE
>>> dis.dis('from sound.effects import echo as echo2')
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (('echo',))
              4 IMPORT_NAME              0 (sound.effects)
              6 IMPORT_FROM              1 (echo)
              8 STORE_NAME               2 (echo2)
             10 POP_TOP
             12 LOAD_CONST               2 (None)
             14 RETURN_VALUE

of these 4 import statements, only import as generates the LOAD_ATTR bytecode; because we have deleted the attribute from the parent module, this bytecode will fail;

this bug creates a discrepancy between import and import as, which is bad and misleading; so it has been fixed in python 3.7:

>>> dis.dis('import sound.effects.echo')
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sound.effects.echo)
              6 STORE_NAME               1 (sound)
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE
>>> dis.dis('import sound.effects.echo as echo2')
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sound.effects.echo)
              6 IMPORT_FROM              1 (effects)
              8 ROT_TWO
             10 POP_TOP
             12 IMPORT_FROM              2 (echo)
             14 STORE_NAME               3 (echo2)
             16 POP_TOP
             18 LOAD_CONST               1 (None)
             20 RETURN_VALUE
>>> dis.dis('from sound.effects import echo')
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (('echo',))
              4 IMPORT_NAME              0 (sound.effects)
              6 IMPORT_FROM              1 (echo)
              8 STORE_NAME               1 (echo)
             10 POP_TOP
             12 LOAD_CONST               2 (None)
             14 RETURN_VALUE
>>> dis.dis('from sound.effects import echo as echo2')
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (('echo',))
              4 IMPORT_NAME              0 (sound.effects)
              6 IMPORT_FROM              1 (echo)
              8 STORE_NAME               2 (echo2)
             10 POP_TOP
             12 LOAD_CONST               2 (None)
             14 RETURN_VALUE

bytecode LOAD_ATTR has been replaced with IMPORT_FROM, which basically does the same thing, but doesnt try to read from the attribute; so if you run the above test case in python 3.7, all 4 statements will pass;

note that there is a minor difference with IMPORT_FROM: it leaves the parent module on the stack; so this needs to be manually popped off with additional bytecodes ROT_TWO and POP_TOP; these 2 bytecodes interchange the top 2 items and then pop top item, the end result is the 2nd item is popped;

references