Problem
I believe that the current interaction between packages, scripts and relative imports to be somewhat unintuitive.
Examples:
- Package Relative Imports - double dot doc example not working - #2 by Mholscher
- python - beyond top level package error in relative import - Stack Overflow
I have an idea that would be more intuitive IMO.
In my view these posts all stem from a single misunderstanding. People expect a project folder to be treated similarly to sub-folders; that a sub-package is self-same to the project folder, just further down the file tree. And then they try to do a relative import and get an error that confuses them:
ImportError: attempted relative import beyond top-level package
Or
ImportError: attempted relative import with no known parent package
They think to themself: Wait, aren’t I working on a package? Isn’t that precisely what I’m doing? Why isn’t my project folder a “package”?
This most often seems to come up with writing tests, because the recommended file structure for test files leads to this situation.
Examples of intuition
Let’s say we have the following structure:
project_folder/
|-- __init__.py
|-- outer_script.py
|-- module_a.py
|-- module_b/
|-- __init__.py
|-- inner_script.py
|-- has_sibling_import.py # from . import module_y
|-- has_parent_import.py # from .. import module_a
|-- module_y.py
|-- module_z/
|-- __init__.py
|-- has_parent_import.py # from .. import module_y
Let’s look at what’s allowed:
python outer_script.py
is allowed toimport module_b.has_sibling_import
.cd .. && python -m project_folder.outer_script
is allowed tofrom .module_b import has_parent_import
python outer_script.py
is allowed toimport module_b.module_z.has_parent_import
.
And what’s not allowed:
python module_b/inner_script.py
can runimport has_sibling_import
, but it will fail because of the relative import contained within.python outer_script.py
can runimport module_b.has_parent_import
, but it will fail because of the relative import contained within.python module_b/inner_script.py
can runimport module_z.has_parent_import
, but it will fail because of the relative import contained within.
This seems pretty clearly unintuitive to me. In my opinion, because python module_b/inner_script.py
is allowed to use import has_sibling_import
, it sets up an expectation that it is implicitly, by default, already using a kind of relative import. But then the from . import module_y
inside that file is sometimes allowed, and sometimes not, and people get confused.
Proposal
So, my idea is: why don’t we just make it work like people expect it to work in the simple cases? Why don’t we make an assumption and implicitly (or explicitly) treat a particular folder as if it were a package?
A few options I see (in order of what I consider to be most intuitive):
- The folder the script lives in.
- The folder the script is run from.
- Something in sys.path or PYTHONPATH.
Or, it could explicitly be a flag when running python for backwards compatibility? python -p outer_script.py
If at least one of these was implicitly a package, and allowed relative imports, I think it would dramatically reduce the number of people who get confused in the first place. And for the people who try to do something that jumps up one step further, I think talking about “packages” will make more sense to explain why it’s not allowed.
What do you guys think? Is there some reason this is a bad idea? Am I wrong about intuitiveness?