And please, just don't use * imports. It really doesn't save you much time at the cost of implicit untraceable behavior. If you don't worry about * imports, you don't need to add the __all__ boilerplate to every module.
This article is more about advertising a package called tach, that I suppose tries to add "true" private classes to Python.
But it doesn't actually enforce anything, because you still need to run their tool that checks anything. You could just easily configure your standard linter to enforce this type of thing rather than use a super specialized tool.
A direct benefit of using `__all__` at module A is better intellisense while editing files that imports A, if A has a small intended public API and many internal usage symbols.
Python's imports are the worst I've seen in any mainstream programming language by far.
Relative path imports are unnecessarily difficult, especially if you want to import something from a parent directory. There's no explicit way to define what you'd like to export either.
> Relative path imports are unnecessarily difficult, especially if you want to import something from a parent directory.
This is never explained well to beginners but this is because Python imports deal with modules and packages, any relationship with directories is accidental. With namespace packages foo.bar and foo.baz might live on totally separate parts of the filesystem. They might not live on the filesystem at all and instead be in a zip file.
somedir/
otherdir/
__init__.py
foo.py
scripts/
__init__.py
myscript.py
# in myscript.py
from .. import otherdir.foo
If you `python scripts/myscript.py` then it won't work because myscript.py is run with a __name__ of __main__ which doesn't have any parent module.
When you run python -m scripts.myscript it sets __package__ to scripts.myscript so now it does have a parent module and somedir and otherdir are importable under the root module because '.' is added to the search path by default.
The worst thing is that it can't handle circular imports, unlike every other language, AFAIK.
I've been told it's because of some deep part of Python interpretation that just can't be changed. The mutable default argument madness is also caused by that.
Python is just plain unsuitable for any project larger than a couple of files.
Every Python project contains a hidden and deadly complexity that will grow over time - and will eventually destroy it. There's no way around it, it creeps in no matter what you do. The imports situation is only part of it - it wasn't what killed our simulation tools, or our build scripts, or our test framework - and required that we rewrite them all in different and more suitable languages.
Python's performance, global modules, whitespace, untyped-by-default code are all killers. You pretty much have to use virtual environments to permit isolation between the multiple differently-versioned sets of dependent packages that you'll need for any project of any complexity, which are a cumbersome and painful solution to a problem that simply shouldn't exist.
It may technically be possible to write clean and maintainable code in Python if you try hard enough, but you're always skating so close to the edge that eventually somebody is going to get in there tip the whole fragile mess into the abyss.
I’ve led the maintenance and development of a medium sized (100s of kloc) Python codebase over a period of five years. As it matured, the code became significantly less fragile and more maintainable. Using private members of modules (or any other object) turned into way less of a problem as we improved our design. Turns out the problems were mostly around I/O and mutable state. We fixed those problems by pushing side effects and mutation as far out to the edge of the codebase as possible. If your core data processing routines don’t have side effects and don’t mutate global state, who cares who the caller is?
Don't know about using __all__ for introspection, but I have found it immensely useful for organizing, reading, and communicating code. When a package has a bunch of files inside of it, but only a handful of names exposed in __all__ it helps a lot with orienting yourself around the package.
It would be interesting to compare this with an alternative based on static analysis.
The Python ecosystem has many standard tools nowadays to enforce consistent style, including how modules import each other. The ast and libcst modules are very fast and can quickly identify any imported symbols beginning with an underscore:
from a import _naughty
It’s also quite possible to build a list of symbols that were imported and ensure that their underscore-prefixed attributes are not accessed:
import a
a._naughty()
You could get creative I suppose…
import a
f = next(
getattr(a, f“_{s}”)
for s in synonyms(“cheeky”)
)
f()
…but at that point one hopes that, as a last resort, ones reviewers would cry foul.
Yup that's what I do too. Works great. And nbdev auto-generates __all__ so I don't have to think about anything -- it all just works.
(Besides which, when you do have __all__ just regular wildcard imports work well anyway. I've been using them for >10 years without trouble. I think people just repeat the claim that they're a problem without having 1st-hand experience of using them with __all__ correctly.)
Eh. Python does indicate public and private APIs. "Name" is public. "_Name" is internal, but you can mess with it if you need to. "__Name" is private, and if you go there anyway and the thing explodes and gives you a bad haircut, you were warned.
Python's position is that we're all adults here. Don't touch "_Name". Really don't touch "__Name". You can if you're an expert and willing to take responsibility for your actions.
Addendum: And in this ModuleWrapper thing, I can still access `core._module.PrivateApi` so we're back to square one.
In my experience, Python devs rarely use the underscore.
Package authors are pretty good about it. But when the only people using your code are your coworkers, people don't seem to put as much thought into having a clean interface.
[+] [-] trainfromkansas|1 year ago|reply
And please, just don't use * imports. It really doesn't save you much time at the cost of implicit untraceable behavior. If you don't worry about * imports, you don't need to add the __all__ boilerplate to every module.
This article is more about advertising a package called tach, that I suppose tries to add "true" private classes to Python.
But it doesn't actually enforce anything, because you still need to run their tool that checks anything. You could just easily configure your standard linter to enforce this type of thing rather than use a super specialized tool.
[+] [-] claxo|1 year ago|reply
[+] [-] matsz|1 year ago|reply
Relative path imports are unnecessarily difficult, especially if you want to import something from a parent directory. There's no explicit way to define what you'd like to export either.
The syntax is inconsistent, too:
vs. (modern JS) Even C/C++ make more sense here.[+] [-] Spivak|1 year ago|reply
This is never explained well to beginners but this is because Python imports deal with modules and packages, any relationship with directories is accidental. With namespace packages foo.bar and foo.baz might live on totally separate parts of the filesystem. They might not live on the filesystem at all and instead be in a zip file.
If you `python scripts/myscript.py` then it won't work because myscript.py is run with a __name__ of __main__ which doesn't have any parent module.When you run python -m scripts.myscript it sets __package__ to scripts.myscript so now it does have a parent module and somedir and otherdir are importable under the root module because '.' is added to the search path by default.
[+] [-] simonw|1 year ago|reply
Python imports made sense straight away - but then I've been a Python programmer for a long time so I'm far removed from a beginner's perspective.
[+] [-] drhagen|1 year ago|reply
[+] [-] BurningFrog|1 year ago|reply
I've been told it's because of some deep part of Python interpretation that just can't be changed. The mutable default argument madness is also caused by that.
Anyone know more?
[+] [-] plasticeagle|1 year ago|reply
Every Python project contains a hidden and deadly complexity that will grow over time - and will eventually destroy it. There's no way around it, it creeps in no matter what you do. The imports situation is only part of it - it wasn't what killed our simulation tools, or our build scripts, or our test framework - and required that we rewrite them all in different and more suitable languages.
Python's performance, global modules, whitespace, untyped-by-default code are all killers. You pretty much have to use virtual environments to permit isolation between the multiple differently-versioned sets of dependent packages that you'll need for any project of any complexity, which are a cumbersome and painful solution to a problem that simply shouldn't exist.
It may technically be possible to write clean and maintainable code in Python if you try hard enough, but you're always skating so close to the edge that eventually somebody is going to get in there tip the whole fragile mess into the abyss.
[+] [-] sevensor|1 year ago|reply
[+] [-] nick238|1 year ago|reply
[+] [-] Daishiman|1 year ago|reply
[+] [-] aatarax|1 year ago|reply
[+] [-] InfoSecErik|1 year ago|reply
[+] [-] klyrs|1 year ago|reply
[+] [-] zokier|1 year ago|reply
Also: https://docs.astral.sh/ruff/rules/import-private-name/
[+] [-] anamexis|1 year ago|reply
[+] [-] gorgoiler|1 year ago|reply
The Python ecosystem has many standard tools nowadays to enforce consistent style, including how modules import each other. The ast and libcst modules are very fast and can quickly identify any imported symbols beginning with an underscore:
It’s also quite possible to build a list of symbols that were imported and ensure that their underscore-prefixed attributes are not accessed: You could get creative I suppose… …but at that point one hopes that, as a last resort, ones reviewers would cry foul.[+] [-] nuttingd|1 year ago|reply
[+] [-] jph00|1 year ago|reply
(Besides which, when you do have __all__ just regular wildcard imports work well anyway. I've been using them for >10 years without trouble. I think people just repeat the claim that they're a problem without having 1st-hand experience of using them with __all__ correctly.)
[+] [-] kstrauser|1 year ago|reply
Python's position is that we're all adults here. Don't touch "_Name". Really don't touch "__Name". You can if you're an expert and willing to take responsibility for your actions.
Addendum: And in this ModuleWrapper thing, I can still access `core._module.PrivateApi` so we're back to square one.
[+] [-] 0x63_Problems|1 year ago|reply
Package authors are pretty good about it. But when the only people using your code are your coworkers, people don't seem to put as much thought into having a clean interface.
[+] [-] VeejayRampay|1 year ago|reply
[+] [-] efilife|1 year ago|reply
[+] [-] VeejayRampay|1 year ago|reply