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.
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
.
>>> 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__
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.
>>> 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
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.
>>> 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!
>>> 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.
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.
>>> 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:
>>> 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
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.
>>> 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.