<3 Python Imports
N.B. This post was migrated from oli-hall.github.io to oli-hall.com on 18/04/2019
I had some interesting fun with Python imports today. Long story short, I'm developing a Python package, with a series of classes declared in appropriately named modules, each of which is exposed at the package level for easy import elsewhere. Something like this:
package
|- __init__.py
|- class_a.py
|- class_z.py
My init.py looks something like this:
from .class_a import A
from .class_z import Z
All fine, no worries. However, I then added a new class, let's call it B, that required Z as a dependency. That's no issue, create a class_b.py, declare B in there, and add a from package import Z. Done! A quick addition of from .class_b import B to init.py and my new class is exposed at package level.
Let the shenanigans commence
I then started using my updated package, and immediately started to get errors about importing Z:
In [1]: from package import Z
–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
ImportError Traceback (most recent call last)
<ipython-input-1-7b646b7ea075> in <module>()
----> 1 from package import Z
<package_dir>/package/__init__.py in <module>()
1 from .class_a import A
----> 2 from .class_b import B
3 from .class_z import Z
<package_dir>/package/class_b.py in <module>()
----> 1 from class_z import Z
2
3
ImportError: cannot import name Z
That's odd. I added Z a while ago, and have been using the package for a while. That class should be fine. Anyhow, I started wondering about whether there was some library caching going on - the package is built as an .egg - and thus the new class hasn't been picked up correctly. I nuked build directories, .egg-infos, everything. Still no change. Finally, I nuked the virtualenv directory, and as a last resort, deleted the entire local copy of the repository and recloned. Still no change.
Fortunately, at this point, I had a lucky guess. See, I had run PyCharm's 'Optimize Imports' to organise imports. This had rearranged the imports in my init.py, so that they were as follows:
from .class_a import A
from .class_b import B
from .class_z import Z
However, B imports Z at the package level. So Python, when evaluating this, tries to import Z from the package level in the process of importing B, which fails, as it's going line-by-line, and hasn't yet imported Z. A quick test was to swap the second and third lines in my init file. Sure enough, importing Z first (which doesn't depend on any other files in the package) made it visible at package level, so that B could then import it.
That's not a particularly great permanent solution though - another 'Optimize Imports' could easily nuke it. Instead, I've now ensured that all inter-package imports are explicit e.g. from package.class_z import Z, eliminating any ordering constraints between the package level imports. Not a complex bug, but interesting nevertheless!