As a Python 2 developer at dayjob, this made me very wary. While we havenāt made a concerted effort to port to Python 3 yet, by far our most common from __future__ statement is absolute_imports, because some conflicts are just too much of a pain to solve without it.
However, you do bring up some mitigating factors later on.
For one, there is an explicit syntax for absolute imports, making it possible to disambiguate. This is one of the big things missing in Python; once you encounter the conflict, thereās no good way around it without renaming something.
One other mitigating factor that you donāt mention is that in Rust, you have to declare a module with mod foo, while in Python itās implicit based on the path name. That means that in Rust, to get ambiguity from a module as the same name as a crate or prelude item, you will have both of the declarations in the same file, which makes it a lot easier to see. Many of the frustrating issues in Python come from some refactoring, that suddenly leads to this issue, and itās with a module youāve forgotten is even present on disk, and itās just complaining about some item not existing that you know exists in the module of the same name that youāre looking for.
Another difference between Rust and Python that I think would mitigate this is how relative imports are treated in Rust and Python. In Rust, they are relative to self; so use mod::something is using something that is defined within the current module, and which is a sub-part of the current module; itās pretty obvious that itās right there. In Python, they are relative by directory path; you are importing from a sibling module, unless you are in an __init__.py file for the root of the package. So from mod import something is more equivalent to use super::mod::something in Rust. The fact that you could accidentally import from a sibling module that you didnāt even know about (since maybe itās a big application, with lots of little modules maintained by different people) is one of the things that makes this a problem in Python, but a module that is in scope within a Rust module is something that has either been declared within it, or explicitly imported, which also helps to make it more obvious what youāre referring to, and where potential conflicts might be coming from, than in Python.
Making it a hard error also helps with issues in which you accidentally run into ambiguity, as then you will immediately see that thereās an ambiguity, not a statement that some item is not found in a module that you expect to find it.
If the hard error is relaxed, and instead there is a disambiguation order, you will want to ensure that error messages about items not existing are explicit about the exact module being discussed, and they should probably also show the other module which does have the named item that was shadowed, to make it easy to debug the problem.
Glob imports can also make things a lot harder to track down in these conflicting cases. Hereās a case we had recently in our Python codebase that blocked another developer for a while, and I needed to help sort it out, in part because Python doesnāt provide very good errors when things are shadowed. Iāll use some simple aliases for the modules in question.
-
app
-
gui
async_utils
particular_window
-
common
messages
In particular_window, we had:
from app.gui import async_utils
from app.messages import *
But in messages, we had:
from app.common import *
Which meant that app.common.async_utils now shadowed the earlier import of app.gui.async_utils.
Letās see what will save us from this issue in Rust:
-
Imports are not exported publicly by default, so the fact that someone had done from app.common import * wouldnāt actually re-export things.
-
Rust actually errors out on importing the same name into the same scope more than once, rather than shadowing.
-
After writing this example out, I realize itās not actually an example of ambiguous relative vs. absolute item paths, but just another downside of glob imports and shadowing. However, glob imports could lead to this kind of thing happening more often, since you would be able to have:
use somecrate::prelude::*;
use util;
And then get ambiguity errors if somecrate::prelude includes util. Of course, glob imports already have issues with conflicting names, this just adds one more place where they could.
On the whole, I think that given the fact that Rust has explicit exporting of names publicly, better error reporting by not allowing imports to shadow each other, and the fact that there is explicit syntax for both types of import, I think that on the whole the proposal sounds more reasonable in Rust than it is in Python 2.
One reason why it might be more desirable in Rust than in Python is also that they syntax for disambiguating is more heavyweight; in Python, itās from name import something vs. from .name import something for absolute vs. relative, while in Rust with only explicit syntax it would be use ::name::something (or even use extern::name::something, depending on the exact syntax chosen) vs. use self::name::something, which is a lot more cumbersome than just use name::something.
One of the biggest reasons Iād be in favor of this proposal is that it would increase the compatibility with Rust 2015 (for those cases where things worked at the top level of a crate without self, which I would imagine a decent number of people with simple single-module applications might have done), and decrease the migration burden/churn. If done along with the backwards-compatible :: behavior, very little code would be required to change. I think the only required changes would be places where the new design introduced an ambiguity that wasnāt present in 2015 so you would have to disambiguate, and there would be optional changes in 2018 where you could drop extra :: qualifiers or shift everything to the āabsolute imports everywhereā style more easily.
In general, even in editions which allow some more types of breaking change than ordinary releases, I think itās best to keep mandatory code churn low, as even fairly simple mechanical changes can make āgit blameā information harder to trace, have the possibility of introducing bugs if there was some manual component to the transition process, make the code no longer compatible with older compilers that people might be using on LTS Linux distros, and so on.