The Trouble with __all__

  • __all__ is only relevant for * imports.

    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.

  • 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.

    The syntax is inconsistent, too:

        from X import Y
        import Z
    
    vs. (modern JS)

        import { Y } from 'X';
        import * as Z from 'Z';
    
    
    Even C/C++ make more sense here.

  • 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.

  • 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.

  • The author seems to expect someone to be patrolling imports with a gun rather than a strongly-worded "we're not liable if you hurt yourself" sign.

  • If you think this sort of hack is going to keep your python codebase clean I got some bad news for ya.

    Also: https://docs.astral.sh/ruff/rules/import-private-name/

  • 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.

  •   import my_module
    
    This is compatible with `__all__` if you have your code broken down into smaller sub-modules and collect them in the main module as follows:

      # my_module/__init__.py
      from .submodule import *
      from .another_submodule import *

  • 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.

  • This is a problem with the language

  • once again, python being the absolute worst amongst all widely used languages