August 1st, 2024

The Trouble with __all__

The article addresses challenges with Python's `__all__` attribute for public APIs, introducing a `ModuleWrapper` class and the Tach tool for enforcing strict interfaces and improving module boundary clarity.

Read original articleLink Icon
The Trouble with __all__

The article discusses the challenges associated with using the `__all__` attribute in Python modules to define public APIs. While PEP 8 recommends using `__all__` for better introspection, it does not enforce access restrictions, leading to potential issues with tightly coupled modules. The author notes that many Python developers neglect to specify public and private interfaces, which can result in significant complications, especially in collaborative environments. To address these issues, the article introduces a custom `ModuleWrapper` class that enforces public interface access based on `__all__`. This approach requires the use of a function, `enable_strict_imports`, to be called before any imports, which can impact runtime performance.

The article then presents a more efficient solution using a tool called Tach, which allows developers to declare modules and enforce strict interfaces through static analysis without runtime overhead. By using Tach, developers can validate module dependencies and ensure that only public interfaces are imported, thus preventing unauthorized access to private APIs. The author emphasizes the importance of maintaining clear module boundaries to avoid costly development issues. The article concludes by inviting feedback from readers who have faced similar challenges and highlights Gauge's mission to address the monolith/microservices dilemma.

Related

Interface Upgrades in Go (2014)

Interface Upgrades in Go (2014)

The article delves into Go's interface upgrades, showcasing their role in encapsulation and decoupling. It emphasizes optimizing performance through wider interface casting, with examples from io and net/http libraries. It warns about complexities and advises cautious usage.

Common Interface Mistakes in Go

Common Interface Mistakes in Go

The article delves into interface mistakes in Go programming, stressing understanding of behavior-driven, concise interfaces. It warns against excessive, non-specific interfaces and offers guidance from industry experts for improvement.

An analysis of module names inside top PyPI packages

An analysis of module names inside top PyPI packages

The blog post emphasizes Python package naming conventions, mapping module names to package names, and analyzing PyPI data. Insights include normalized names, common prefixes/suffixes, and advice for developers to follow conventions and avoid namespace packages.

The Python linter Ruff is a win for open source – and Rust

The Python linter Ruff is a win for open source – and Rust

The Python linter Ruff is praised for its role in open source and Rust programming. The article emphasizes data transparency in AI projects, expert contributions, and the tech industry's evolution towards open source, AI, and data management.

Types as Interfaces

Types as Interfaces

The article explores using wrapper types like Msg and Timestamped in Haskell to annotate data without modifying existing types directly. It discusses challenges in composing annotated types and suggests using typeclasses for solutions. Emphasizes simplifying code for essential variants.

Link Icon 11 comments
By @trainfromkansas - 3 months
__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.

By @matsz - 3 months
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.
By @plasticeagle - 3 months
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.

By @aatarax - 3 months
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.
By @InfoSecErik - 3 months
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.
By @zokier - 3 months
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/

By @gorgoiler - 3 months
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.
By @nuttingd - 3 months

  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 *
By @kstrauser - 3 months
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.

By @efilife - 3 months
This is a problem with the language
By @VeejayRampay - 3 months
once again, python being the absolute worst amongst all widely used languages