I love this proposal because it seems to be the most Pythonic approach to access modifiers (or, if you prefer, a strong access suggestion, since it can still be bypassed) that I’ve seen so far.
I don’t have particularly strong feelings about the syntax. Whether this ends up being a decorator, a keyword, or a global dunder matters much less to me than the idea itself. That said, I’ll use __export__ throughout this comment because that’s what the PEP uses.
On underscores
It doesn’t take much looking around before you find code where _name does not mean “private”. The first example that comes to mind is typing.NamedTuple (and collections.namedtuple), which expose several public methods that break the convention, such as _make, _replace, and _asdict.
I think Python code is beautiful. I genuinely enjoy writing Python. What I don’t enjoy is making my code noisier just to communicate that something is internal, especially when it isn’t even a strong enough convention for tooling to support.
I’ve ended up adopting a simple policy in my own libraries: anything not listed in __all__ is considered private, and I don’t bother prefixing those names with underscores.
On tooling
As of writing this, only pyright with typeCheckingMode = "strict" warns about this snippet:
from functools import _make_key
_ = _make_key((), {}, typed=False)
functools prefixed the function with an underscore and omitted it from __all__, yet mypy, pyrefly, and zuban (all in strict mode) do not report an error. ty also accepts this snippet, although it does not currently offer a strict mode.
Autocomplete is no different. Pyright, pyrefly, and ty will all happily suggest _make_key when you type functools..
I don’t know why most type checkers and language servers have chosen not to warn about or hide _name, but if I had to guess, it’s because a simple rule would produce too many false positives.
Like everyone else here, I spend a lot of time in my IDE. Consequently, I care quite a bit about the IDE experience. It’s one of the reasons I embraced typing so heavily. I can stay in flow when my IDE is helping me. What breaks that flow is having to stop and consult documentation for something that could have been communicated through immediate feedback.
On whether this is Pythonic
I don’t see how this is unpythonic. You’re still free to poke at internals. This proposal just makes it obvious that you’re stepping outside the supported API.
Good APIs should be harder to misuse. The stdlib has plenty of examples where “read the docs” wasn’t enough. datetime.utcnow() documented that it returned naive objects since Python 3.0, yet it was still eventually deprecated because it was too easy to misuse. Documentation alone wasn’t sufficient. I don’t think anyone would argue that we should keep easy-to-misuse APIs because people should just read the docs and not mess up.
On how libraries solve this today
One thing I’ve found interesting while reading this thread is the idea that Python has always had unrestricted access to internals. I don’t think that’s ever really been true. Library authors have always been able to decide what is part of their public API and what isn’t. The only difference is that pure Python libraries have had to build that machinery themselves.
JAX has already been mentioned several times in this thread, so here are a few more examples:
The unfortunate part is that none of these approaches can ever provide a great developer experience. Every library ends up implementing its own flavor of access modifiers, and tooling can’t reasonably be expected to understand all of them. At best, you get runtime behavior. At worst, you end up duplicating your public API under if TYPE_CHECKING just to keep static analyzers happy.
On extension modules
Extension modules have complete control over what they expose and can trivially prevent users from reaching their internals. Everything is private by default, and they explicitly choose what to expose to Python.
So I don’t really buy the argument that Python has never had access modifiers. Extension modules have effectively had them for decades. It’s only pure Python libraries, or the pure Python portions of mixed libraries, that lack an equivalent way to express the same intent.
__export__ is just bringing a capability that extension modules have had for decades to pure Python as well. I don’t think anyone will agree with me if I said extension modules are unpythonic because they don’t allow me to poke their internals.
One thing I’d change
The only part of the proposal I’d change is the warning.
Personally, I strongly preferred the original ImportError (or a subclass of it). If the end result is only a warning, then I honestly think most of the runtime behavior could be dropped entirely, and this could instead become a typing PEP that teaches type checkers, IDEs, language servers, and other tooling to understand __export__.
On LLMs
Whether you see this as a positive or a negative is up to you, but __export__ would improve LLM-based workflows. If they accidentally reach for an internal API, immediate and actionable feedback is much more useful than silently generating code against an unsupported interface.