2

I met this line of code from an __init__.py file. Can anyone explain to me what the last two lines mean? I understand that __all__ is an empty list at the start. In particular have a hard time understanding the arguments apps.__all__ and 'apps', and why there is a need to extend when there is already append, or vice versa.

__all__ = []

from . import apps
from .apps import *
__all__.extend(apps.__all__)
__all__.append('apps')
kwaldner
  • 95
  • 1
  • 6
  • 2
    It doesn't have to be done this way. The code dynamically collects the apps modules and appends them to its own magic variable `__all__` to export them. With this trick you don't have to update `init.py` when you add a new module. – Robert Jul 11 '21 at 17:18

2 Answers2

2

TL;DR: It doesn't, it's just for convenience when the package is too large or when you want to allow a star-import for a user.

It extends the default allowed symbols ([] for you, see __all__ value) for the star import (from x import *) so it contains all of the pre-defined symbols from apps.__all__ and also apps itself as well.

That means if your module is called hello and I do this:

# apps.py
__all__ = [x, y, z]

# __init__.py
__all__ = []
from . import apps
from .apps import *
__all__.extend(apps.__all__)
__all__.append('apps')

# main.py
from hello import *
# imports __init__ and executes unguarded lines
# __all__ in __init__.py is initialized
# apps.py is imported
#   apps.py code is executed
#   __all__ for apps.py is assembled
# star-import from apps.py imports all allowed symbols from apps.py
# __all__ is extended with "apps" and apps.__all__
# then your star-import in main.py would expose all of the listed symbols

I'll have these symbols available as "variables" in the module I issue the import in:

x
y
z
apps

i.e. I can then reference in my main.py: x, y, z contained in the apps.py module directly or I can reference them through the apps value (main.apps after star import) like this: apps.x, apps.y, apps.z.

If you do not specify the __all__ you can import everything as the __all__ variable serves only as a guard and this way of using it in the __init__.py of a package ensures you can star-import just by package's name and have all of the necessary functions, constants, etc present without knowing the package's structure. However it can be also problematic with circular imports.

Peter Badida
  • 11,310
  • 10
  • 44
  • 90
2

As stated here:

The import statement uses the following convention: if a package’s __init__.py code defines a list named __all__, it is taken to be the list of module names that should be imported when from package import * is encountered.

In last two lines, you are filling the __all__.

Because apps.__all__ is also a list and you want the items inside that list to be appended we use .extend(), otherwise the list itself appends to __all__.

Next we include the name of the submodule(or package if it has __init__.py) that is going to be imported which is apps. (Remember we imported apps module/package in line 3), so it's in global namespace now.

By doing those 2 lines, when you import the current package, You have not only items of the apps.__all__ but also apps module/package itself availabe in namespace of wherever the import statement is.

S.B
  • 13,077
  • 10
  • 22
  • 49
  • thanks! that makes sense, and I understand that `apps.__all__` means that the apps directory contains the `__init__.py` file? And what does `.apps` mean? Could I write something like from . import apps from .apps import * – kwaldner Jul 11 '21 at 17:29
  • 1
    @kwaldner Normally a package defines `__all__` in it's `__init__.py` to handle what it wants to export. Also modules can have `__all__`. Whenever you see dot before the name of the packages in import statement you're using "relative imports" and single dot means current package, `..` means the parent package. – S.B Jul 11 '21 at 17:37
  • 1
    Thanks! I understand now. :) – kwaldner Jul 11 '21 at 18:04