You Should Use __all__

In the latest Python Weekly issue 607, there was a well written article by James Turk called You Don’t Need __all__ (waybackmachine backup link) which seemed to miss an important point I wanted to discuss. With no way to comment on his blog, I decided might as well turn it into a learning experience here!

Where We Agree

James makes a lot of good points throughout his article and explains how imports work well, so please do give it a quick read through! We are step in step with the mindset that you probably shouldn’t be using import * anyways. It’s messy and unclear.

However everyone writing Python code isn’t in a perfect dev environment. A lot are people just trying to run stuff quickly in notebooks to produce a report, new to Python, or frankly just like the convenience of using import *.

Where You Need __all__

Something not covered in the article is what __all__ is really handy for. It’s too limit the imports from a module. Including other imports.

Let’s say you have this messy.py code.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
from json import loads
import shutil
import pathlib
def my_reverse_function(my_var: str) -> str:
return my_var[::-1]
from json import loads import shutil import pathlib def my_reverse_function(my_var: str) -> str: return my_var[::-1]
from json import loads
import shutil
import pathlib

def my_reverse_function(my_var: str) -> str:
    return my_var[::-1]

If you use from messy import * you aren’t only going to get my_reverse_function you will also get all the other imports from that file, including loads, shutil and pathlib.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
>>> from messy import *
>>> print(globals().keys())
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'loads', 'shutil', 'pathlib', 'my_reverse_function'])
>>> from messy import * >>> print(globals().keys()) dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'loads', 'shutil', 'pathlib', 'my_reverse_function'])
>>> from messy import *

>>> print(globals().keys()) 
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'loads', 'shutil', 'pathlib', 'my_reverse_function'])

This is instantly solved by adding __all__

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
from json import loads
import shutil
import pathlib
__all__ = ["my_reverse_function"]
def my_reverse_function(my_var: str) -> str:
return my_var[::-1]
from json import loads import shutil import pathlib __all__ = ["my_reverse_function"] def my_reverse_function(my_var: str) -> str: return my_var[::-1]
from json import loads
import shutil
import pathlib

__all__ = ["my_reverse_function"]

def my_reverse_function(my_var: str) -> str:
    return my_var[::-1]

Notice that loads and the other imports are no longer imported.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
>>> from messy import *
>>> print(globals().keys())
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'my_reverse_function'])
>>> from messy import * >>> print(globals().keys()) dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'my_reverse_function'])
>>> from messy import *         

>>> print(globals().keys()) 
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'my_reverse_function'])

Name Collisions

Say you have a simple file with a custom loads function, I’ll call it my_stuff.py

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import os
def loads(item, value):
"""Load item into environment variables"""
os.environ[item] = value
import os def loads(item, value): """Load item into environment variables""" os.environ[item] = value
import os

def loads(item, value):
    """Load item into environment variables"""
    os.environ[item] = value

So you import it and run it and all is good.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
>>> from my_stuff import *
>>> loads("MY_VAR", "WORLD")
>>> from my_stuff import * >>> loads("MY_VAR", "WORLD")
>>> from my_stuff import * 
>>> loads("MY_VAR", "WORLD")

Then later in your notebook you want that handy reverser function, so you import everything from messy.py. Then you try running loads again and suddenly it broke!

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
>>> from messy import *
>>> loads("MY_VAR", "WORLD")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: loads() takes 1 positional argument but 2 were given
>>> from messy import * >>> loads("MY_VAR", "WORLD") Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: loads() takes 1 positional argument but 2 were given
>>> from messy import *
>>> loads("MY_VAR", "WORLD")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: loads() takes 1 positional argument but 2 were given

Suddenly it’s using the loads from json imported from messy.py because there was no __all__.

Real World Example

So if you are writing a library that other people are going to use, say like python-box that other people use and some will surely misuse it in some way, and don’t want them to hurt themselves or mess up their globals, it’s good to add the __all__ .

Look at all these imports needed in that library.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import copy
import re
import warnings
from keyword import iskeyword
from os import PathLike
from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union
from inspect import signature
try:
from typing import Callable, Iterable, Mapping
except ImportError:
from collections.abc import Callable, Iterable, Mapping
try:
from IPython import get_ipython
except ImportError:
ipython = False
else:
ipython = True if get_ipython() else False
import box
from box.converters import (
BOX_PARAMETERS,
_from_json,
_from_msgpack,
_from_toml,
_from_yaml,
_to_json,
_to_msgpack,
_to_toml,
_to_yaml,
msgpack_available,
toml_read_library,
toml_write_library,
yaml_available,
)
from box.exceptions import BoxError, BoxKeyError, BoxTypeError, BoxValueError, BoxWarning
__all__ = ["Box"]
import copy import re import warnings from keyword import iskeyword from os import PathLike from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union from inspect import signature try: from typing import Callable, Iterable, Mapping except ImportError: from collections.abc import Callable, Iterable, Mapping try: from IPython import get_ipython except ImportError: ipython = False else: ipython = True if get_ipython() else False import box from box.converters import ( BOX_PARAMETERS, _from_json, _from_msgpack, _from_toml, _from_yaml, _to_json, _to_msgpack, _to_toml, _to_yaml, msgpack_available, toml_read_library, toml_write_library, yaml_available, ) from box.exceptions import BoxError, BoxKeyError, BoxTypeError, BoxValueError, BoxWarning __all__ = ["Box"]
import copy
import re
import warnings
from keyword import iskeyword
from os import PathLike
from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union
from inspect import signature

try:
    from typing import Callable, Iterable, Mapping
except ImportError:
    from collections.abc import Callable, Iterable, Mapping

try:
    from IPython import get_ipython
except ImportError:
    ipython = False
else:
    ipython = True if get_ipython() else False

import box
from box.converters import (
    BOX_PARAMETERS,
    _from_json,
    _from_msgpack,
    _from_toml,
    _from_yaml,
    _to_json,
    _to_msgpack,
    _to_toml,
    _to_yaml,
    msgpack_available,
    toml_read_library,
    toml_write_library,
    yaml_available,
)
from box.exceptions import BoxError, BoxKeyError, BoxTypeError, BoxValueError, BoxWarning

__all__ = ["Box"]

But because it uses the __all__ it give the user a very neat import if they call this directly.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
>>> from box.box import *
>>> print(globals().keys())
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'Box'])
>>> from box.box import * >>> print(globals().keys()) dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'Box'])
>>> from box.box import *
>>> print(globals().keys())
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'Box'])

If It didn’t have that, it would look like:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
>>> from box.box import *
>>> print(globals().keys())
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'copy', 're', 'warnings', 'iskeyword', 'PathLike', 'Any', 'Dict', 'Generator', 'List', 'Optional', 'Tuple', 'Type', 'Union', 'signature', 'Callable', 'Iterable', 'Mapping', 'box', 'BOX_PARAMETERS', 'msgpack_available', 'toml_read_library', 'toml_write_library', 'yaml_available', 'BoxError', 'BoxKeyError', 'BoxTypeError', 'BoxValueError', 'BoxWarning', 'NO_DEFAULT', 'NO_NAMESPACE', 'Box'])
>>> from box.box import * >>> print(globals().keys()) dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'copy', 're', 'warnings', 'iskeyword', 'PathLike', 'Any', 'Dict', 'Generator', 'List', 'Optional', 'Tuple', 'Type', 'Union', 'signature', 'Callable', 'Iterable', 'Mapping', 'box', 'BOX_PARAMETERS', 'msgpack_available', 'toml_read_library', 'toml_write_library', 'yaml_available', 'BoxError', 'BoxKeyError', 'BoxTypeError', 'BoxValueError', 'BoxWarning', 'NO_DEFAULT', 'NO_NAMESPACE', 'Box'])
>>> from box.box import *
>>> print(globals().keys())
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'copy', 're', 'warnings', 'iskeyword', 'PathLike', 'Any', 'Dict', 'Generator', 'List', 'Optional', 'Tuple', 'Type', 'Union', 'signature', 'Callable', 'Iterable', 'Mapping', 'box', 'BOX_PARAMETERS', 'msgpack_available', 'toml_read_library', 'toml_write_library', 'yaml_available', 'BoxError', 'BoxKeyError', 'BoxTypeError', 'BoxValueError', 'BoxWarning', 'NO_DEFAULT', 'NO_NAMESPACE', 'Box'])

Defining imports in __init__.py

Again, James does also touch on a great design principal that you should just declare the exports explicitly in your __init__.py file. Notice I am doing from box.box import * ? You shouldn’t be doing that anyways, rather from box import *. Because python-box is already using the technique he described, by just defining the imports they want exposed in the root __init__.py

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
from box.box import Box
from box.box_list import BoxList
from box.config_box import ConfigBox
from box.exceptions import BoxError, BoxKeyError
from box.from_file import box_from_file, box_from_string
from box.shorthand_box import SBox, DDBox
from box.box import Box from box.box_list import BoxList from box.config_box import ConfigBox from box.exceptions import BoxError, BoxKeyError from box.from_file import box_from_file, box_from_string from box.shorthand_box import SBox, DDBox
from box.box import Box
from box.box_list import BoxList
from box.config_box import ConfigBox
from box.exceptions import BoxError, BoxKeyError
from box.from_file import box_from_file, box_from_string
from box.shorthand_box import SBox, DDBox

So if you did from box import * you would only get those specific items, even if they didn’t have the __all__ in the box.py file.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
>>> from box import *
>>> print(globals().keys())
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'Box', 'BoxList', 'ConfigBox', 'BoxError', 'BoxKeyError', 'box_from_file', 'SBox', 'DDBox'])
>>> from box import * >>> print(globals().keys()) dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'Box', 'BoxList', 'ConfigBox', 'BoxError', 'BoxKeyError', 'box_from_file', 'SBox', 'DDBox'])
>>> from box import * 
>>> print(globals().keys())
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', 'Box', 'BoxList', 'ConfigBox', 'BoxError', 'BoxKeyError', 'box_from_file', 'SBox', 'DDBox'])

Why does it matter?

Explicit is better than implicit.
PEP 20

Because if you have code used by other people, this could save them someday without them ever realizing it. Avoiding a hidden name collision, or prevent using wrong function that’s named the same elsewhere. Just think about it, I would bet you’ve seen a lot of functions just named loads, dumps, run or similar.

Yes, it is better to avoid import *, and yes it’s better to have a well structured project, but you can’t stop other’s from using Python as they see fit. And import * is fast and valid. You shouldn’t ignore convention because it’s not convenient to your way of coding if your library is used by others.