While PEP-810 introduced a neat facility for lazy imports, it explicitly chose to omit any error handling, so codes that need to handle import errors must settle with either eager imports in try-excepts (and bear the cost of increased load times) or the workaround of lazily importing wrapper modules that handle import errors (and bear the cost of maintaining those small boilerplate modules that clutter up the repo).
This proposal aims to address the common use cases of handling import errors, which are needed more for lazy imports than for eager imports because it is more unrealistic to expect every first use of a lazy object to be wrapped in a try-except.
While there is already an ongoing discussion with a similar goal in Optional imports for optional dependencies, it focuses more on only one of the common use cases but not the several important others, so I believe a separate discussion is warranted for a more comprehensive solution.
As far as I can see, there are 4 common ways that an import error is handled:
- Import a simple drop-in replacement module:
try:
import regex as re
except ImportError:
import re
- Raise a custom exception:
try:
import yaml
except ImportError:
raise RuntimeError("Please install pyyaml.")
- Retry a failed import after resolving the issue:
try:
import xlrd
except ImportError:
os.system('pip install xlrd')
import xlrd
- Import a mostly compatible replacement module that requires different initialization steps to produce the same set of names and flags (
load_yamlandHAS_RUAMEL_YAMLin the following example):
try:
from ruamel.yaml import YAML
load_yaml = YAML().load
HAS_RUAMEL_YAML = True
except ImportError:
from yaml import load as load_yaml
HAS_RUAMEL_YAML = False
The Proposal
Introduce extended syntaxes for both eager and lazy imports that specify the behaviors upon anImportError.
Addressing the use cases listed above, the extended syntaxes below apply to both eager and lazy imports, in both import and from-import forms:
- Use
[lazy] import <module 1> or <module 2> as <name>to specify a drop-in replacment for a failed import, e.g.:
lazy import regex or re as re
- Use
[lazy] import <module> or raise <exception>to raise a custom exception on a failed import, e.g.:
lazy import yaml or raise RuntimeError("Please install pyyaml.")
- Use
try [lazy] import <module> or: <suite>to specify a suite to perform on a failed import:
try lazy import xlrd or:
os.system('pip install xlrd')
import xlrd
- Use
try [lazy] import <module 1> for <names>: <suite 1>to specify a suite to perform on a successful import in order to produce the names specified in theforclause, which, importantly for lazy imports, will become lazy objects that will trigger reification on access. An optional clause ofor import <module 2>: <suite 2>specifies an alternative import and initialization steps upon a previous import failure in order to produce the same names. And an optional clause ofelse: <suite 3>specifies what to do when all the previous import clauses fail. AnImportErrorshould be raised if an import clause succeeds but does not produce all of the names specified in theforclause. For example:
try lazy from ruamel.yaml import YAML for load_yaml, HAS_RUAMEL_YAML:
load_yaml = YAML().load
HAS_RUAMEL_YAML = True
or from yaml import load as load_yaml:
HAS_RUAMEL_YAML = False
else:
raise RuntimeError('No YAML loader installed.')
assert load_yaml('key: value') == {'key': 'value'}
This is just a rough idea so far and I’d like to hear some preliminary feedbacks from the community before I proceed to formalize the syntaxes and semantics.