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 articleThe 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)
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
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
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 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
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.
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.
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.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.
Also: https://docs.astral.sh/ruff/rules/import-private-name/
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 *
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.
Related
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
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
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 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
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.