pythonpackagepython-import

Using Alias in __init__.py Still Exposes Original Submodule Name in Package Namespace in Python


I noticed unexpected behavior when importing a submodule with an alias in my package's __init__.py file. In short, using an alias in __init__.py still exposes the original submodule name in the package namespace.

Specifically, this is how to reproduce the result:

.
├── package
│   ├── __init__.py
│   └── submodule.py
└── test.py
from . import submodule as sm

print(dir())
import package


print(package.submodule)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'sm', 'submodule']
<module 'package.submodule' from path_to_submodule.py>

What happens

You can see the submodule is exposed even when I import it as an alias in __init__.py.

What I expect

I expect that the test.py should not have access to package.submodule because the __init__.py didn't import it directly. And I am not sure if this is a bug in python.

The point in this question is that the from X import Y as Z doesn't create two variables Y and Z in the normal situation. For example, if I use from package import submodule as sm in test.py, and then print(dir()), the result shows that it doesn't have access to submodule. However, in this question, it actually creates two variables submodule and sm.

I try to ask ChatGPT or other LLMs and search the doc from python.org. However, I didn't get a clear answer. Is this a bug or a normal design?


Solution

  • You need to differentiate between two different cases.

    1. You do not import the module in the __init__.py; then package can also not access it until it is explicitly imported.
    import package
    package.submodule # <-- Error
    
    # OK if imported
    import package.submodule 
    package.submodule
    
    1. You import the module like you do. As soon as you import submodule, alias or not, submodule will be put into the locals of package.__init__

    You might be surprised, but this code works, explanation added at the end:

    import .submodule as sm
    del submodule  # Looks like a NameError but isnt
    

    However, you will get an attribute error in the following code, that nobody would expect:

    import package.submodule
    package.submodule # <-- AttributeError
    

    So theoretically you can just delete the submodule variable, however a) do not rely on it that it will work with all python versions and in the future, and more importantly b) you will create unintuitive code.


    The recommended way is to rename it to _submodule with the _ underscore telling the user that it is private and not intended to be used - some linters might also not show it for auto completion.



    Why does aliasing create two variables?.

    First is that the usage of import and its variants from, as is sugar syntax make use of the __import__ function with some syntactic sugar:

    Roughly from . import submodule as sm executes to:

    # in .
    import submodule
    sm = submodule
    

    To differentiate, this happens in the respective package's __init__ and not the file you are executing.
    For example, from package import submodule as sm, will neither add package nor submodule to your current locals.
    Except, this statement is written in package.__init__ itself.

    Similarly if you have modules a,b,c and do from a.b import c then b will be added to a and c to b.