You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Make _MutuallyExclusiveGroup a public API in argparse #144812
argparse.ArgumentParser.add_mutually_exclusive_group() returns an argparse._MutuallyExclusiveGroup instance, but this type is private (underscore-prefixed). This makes it impossible to properly type-annotate code that stores the return value without importing a private symbol.
Motivation
When using type checkers (mypy, ty, pyright), code like:
is flagged as a type error because _MutuallyExclusiveGroup is not ArgumentParser, but there's no public type to use instead. The only workaround is importing the private _MutuallyExclusiveGroup.
This is similar to #101503 which requests making the return type of add_subparsers public.
Proposal
Make _MutuallyExclusiveGroup (and possibly _ArgumentGroup) public by removing the underscore prefix, or at minimum export a public type alias.
I think typing issues are not a good reason to make inner classes public.
Create a protocol, and use it. This is the right way in dynamically typed programming languages like Python. Or just use non-public classes if you are not afraid that your code can break with future Python versions.
I'm alright with the protocol approach, however I think that should live in CPython. Python is exposing an interface here, so should be able to expose a machine readable variant of it too (which types essentially are, so type checkers know what contract user code should follow).
Exposing an interface does not mean that its implementation should be exposed. We could document that only the name becomes public (like we did for Action) and that it should be treated as a protocol (kinda) but if we were to do something within the stdlib, creating protocols would be better (though, it would come at some cost oj our side).
I'm alright with the protocol approach, however I think that should live in CPython. Python is exposing an interface here, so should be able to expose a machine readable variant of it too (which types essentially are, so type checkers know what contract user code should follow).
I agree.
If the type hints/annotations were defined in argparse.py instead of a pyi file in typeshed, there wouldn't be an issue. One could define a new structural type, MutuallyExclusiveGroupT via Protocol in argparse.py and that would be the end of it.
However, having some of the types/interfaces/protocols defined in typeshed while others defined in cpython argparse.py is probably not a great direction to start going down. Having this bifurcation would be confusing and it's not a great interface for the average user.
I wish there was a clearer path forward to fundamentally address these types of issues.
As the post before me highlighted, these are already widely used within the community. I personally would prefer if he would expose the classes themselves. I'm not entirely sure I follow the argument that exposing the class would make refactoring the code in the future any harder. For what it is worth, we already document inside the documentation all these methods as public methods. Should we change those, we would be introducing a breaking change anyways. I'm all right with the protocol too if we must, but then what else is there to be decided? I did open a pull request, having the impression that we settled that the protocols are the way to go, but it seems I misunderstood the situation. What can we do to push this topic forward? Thank you.
I'm not entirely sure I follow the argument that exposing the class would make refactoring the code in the future any harder
Exposing a public API as a standard library means that any change on the already public API forces us to also make that change in the protocol. So it adds a small maintenance burden.
Now, the problem I have with the PR is that it introduces a complex mechanism to avoid importing Protocol at runtime and so I would prefer having argparse.types. However, having argparse.types means turning argparse into a package which is also a non-trivial change. At least, the precedent would be pathlib.types.
What can we do to push this topic forward? Thank you
Pushing the topic forward may also mean keeping the status quo which is common in this project. Leaving it to a third-party library is also a solution. Usually, typing arguments are also not strong enough to warrant exposing more features as those are left to third-party libraries or to self-maintained user code.
On a personal note: I would be +0.5 for argparse.types so that we can put protocols there without having to worry about startup time, but that decision must be left to (argparse) maintainers such as Serhiy or Savannah and not regular contributors. Any typing addition inside argparse.py directly is, IMO, not a good idea.
Exposing a public API as a standard library means that any change on the already public API forces us to also make that change in the protocol. So it adds a small maintenance burden.
We already exposed this as a public API throughout the documentation. All methods that this protocol would expose are already exposed as public. Just because we made the class private but then documented all of its members public in our docs doesn't make it private in my book.
What I meant by that sentence is that it adds yet-another-class to change if we make changes somewhere else and it doesn't matter how you see it, it remains a maintenance burden. And this is something we want to avoid in the stdlib in general.
And again, my main disagreement with the PR is not the exposure of the protocol itself, it's how it's being exposed and implemented. But the alternative (so as to avoid startup time slowdowns) is argparse.types and this is something I'm not willing to endorse (and hence I am leaving the final decision to argparse maintainers)
@picnixz thanks for laying out the options. Let me try to converge.
I'm fine with either in-tree shape: protocols in argparse.py, or an argparse.types submodule along the lines of pathlib.types. The lazy _build_group_protocols mechanism only exists to keep typing off the import argparse path, and argparse.types solves that by construction, so I'm glad to drop it.
I'd argue against pushing this to a third-party library. The value here is annotating the return of add_argument_group() and add_mutually_exclusive_group() against the parser you already imported from the stdlib. Pulling a separate package to name a type the stdlib hands you is awkward enough that most users will keep importing the private _-prefixed classes, which is the status quo and what ~140 projects already do.
On the maintenance burden: the surface we'd commit to is add_argument, which the docs already describe as public on these objects. Changing it is already a breaking change under our policy. The protocol doesn't add a compatibility promise; it makes the documented one machine-readable.
This needs an argparse maintainer to pick a direction. @serhiy-storchaka / @savannahostrowski, do you prefer (a) protocols in argparse.py or (b) an argparse.types submodule? I'll rework the PR to match, and I'll do the argparse.types packaging if that's the call.
The lazy _build_group_protocols mechanism only exists to keep typing off the import argparse path, and argparse.types solves that by construction, so I'm glad to drop it.
I know. And this is why I'm against having it in argparse.py.
Also, you forgot one solution: (c) do nothing. I'm +0.5 on adding a protocol but if a maintainer doesn'tt want to, we won't add it.
TBH, all of this sounds pretty heavy for what's effectively "align the module with how people already use the API". I feel like a much more pragmatic approach would just be to make ArgumentGroup/MutuallyExclusiveGroup public and keep the _-prefixed names as backwards compat aliases. It's ~2 lines of code in the module, far less maintenance burden and ceremony, and no second copy of the contract to keep in sync.
I could maybe justify the protocol if we genuinely believed we'd someday fully rearchitect these classes but 16 years of status quo says we won't 🙂. And even if we did, we'd have to handle the old names in the same way either way (plenty of code already imports the _-prefixed ones), so the benefit is small IMO. The protocol's only real edge is letting us swap the class identity, which isn't a problem we actually have.
To be clear, if we went this route, I'd still suggest only documenting the already-documented public methods as the supported API. The constructor and inherited internals stay implementation details, so we're not committing to anything the docs didn't already promise. Also, there's prior art for this approach. Here are some examples of some stdlib modules promoting a private class to public while keeping the _-prefixed name as a backwards compat alias (just a quick search!):
I'm happy to go down that path. The only reason I implemented here the protocol approach is because previously on this issue I got pushed back when I suggested to make it public. So if you sign off on making it public, I'm happy to go down that path. It's what I was initially championing for too.
Activity
serhiy-storchaka commentedon Feb 15, 2026
I think typing issues are not a good reason to make inner classes public.
Create a protocol, and use it. This is the right way in dynamically typed programming languages like Python. Or just use non-public classes if you are not afraid that your code can break with future Python versions.
gaborbernat commentedon Feb 15, 2026
I'm alright with the protocol approach, however I think that should live in CPython. Python is exposing an interface here, so should be able to expose a machine readable variant of it too (which types essentially are, so type checkers know what contract user code should follow).
picnixz commentedon Feb 16, 2026
Exposing an interface does not mean that its implementation should be exposed. We could document that only the name becomes public (like we did for Action) and that it should be treated as a protocol (kinda) but if we were to do something within the stdlib, creating protocols would be better (though, it would come at some cost oj our side).
mpkocher commentedon Feb 17, 2026
I agree.
If the type hints/annotations were defined in
argparse.pyinstead of apyifile in typeshed, there wouldn't be an issue. One could define a new structural type,MutuallyExclusiveGroupTvia Protocol inargparse.pyand that would be the end of it.However, having some of the types/interfaces/protocols defined in typeshed while others defined in cpython
argparse.pyis probably not a great direction to start going down. Having this bifurcation would be confusing and it's not a great interface for the average user.I wish there was a clearer path forward to fundamentally address these types of issues.
mpkocher commentedon Feb 17, 2026
https://github.com/search?q=%22argparse._MutuallyExclusiveGroup%22+language%3APython&type=code
gaborbernat commentedon May 30, 2026
As the post before me highlighted, these are already widely used within the community. I personally would prefer if he would expose the classes themselves. I'm not entirely sure I follow the argument that exposing the class would make refactoring the code in the future any harder. For what it is worth, we already document inside the documentation all these methods as public methods. Should we change those, we would be introducing a breaking change anyways. I'm all right with the protocol too if we must, but then what else is there to be decided? I did open a pull request, having the impression that we settled that the protocols are the way to go, but it seems I misunderstood the situation. What can we do to push this topic forward? Thank you.
picnixz commentedon May 30, 2026
Exposing a public API as a standard library means that any change on the already public API forces us to also make that change in the protocol. So it adds a small maintenance burden.
Now, the problem I have with the PR is that it introduces a complex mechanism to avoid importing Protocol at runtime and so I would prefer having argparse.types. However, having argparse.types means turning argparse into a package which is also a non-trivial change. At least, the precedent would be
pathlib.types.Pushing the topic forward may also mean keeping the status quo which is common in this project. Leaving it to a third-party library is also a solution. Usually, typing arguments are also not strong enough to warrant exposing more features as those are left to third-party libraries or to self-maintained user code.
picnixz commentedon May 30, 2026
On a personal note: I would be +0.5 for argparse.types so that we can put protocols there without having to worry about startup time, but that decision must be left to (argparse) maintainers such as Serhiy or Savannah and not regular contributors. Any typing addition inside
argparse.pydirectly is, IMO, not a good idea.gaborbernat commentedon May 30, 2026
We already exposed this as a public API throughout the documentation. All methods that this protocol would expose are already exposed as public. Just because we made the class private but then documented all of its members public in our docs doesn't make it private in my book.
picnixz commentedon May 31, 2026
What I meant by that sentence is that it adds yet-another-class to change if we make changes somewhere else and it doesn't matter how you see it, it remains a maintenance burden. And this is something we want to avoid in the stdlib in general.
picnixz commentedon May 31, 2026
And again, my main disagreement with the PR is not the exposure of the protocol itself, it's how it's being exposed and implemented. But the alternative (so as to avoid startup time slowdowns) is
argparse.typesand this is something I'm not willing to endorse (and hence I am leaving the final decision to argparse maintainers)gaborbernat commentedon May 31, 2026
@picnixz thanks for laying out the options. Let me try to converge.
I'm fine with either in-tree shape: protocols in
argparse.py, or anargparse.typessubmodule along the lines ofpathlib.types. The lazy_build_group_protocolsmechanism only exists to keeptypingoff theimport argparsepath, andargparse.typessolves that by construction, so I'm glad to drop it.I'd argue against pushing this to a third-party library. The value here is annotating the return of
add_argument_group()andadd_mutually_exclusive_group()against the parser you already imported from the stdlib. Pulling a separate package to name a type the stdlib hands you is awkward enough that most users will keep importing the private_-prefixed classes, which is the status quo and what ~140 projects already do.On the maintenance burden: the surface we'd commit to is
add_argument, which the docs already describe as public on these objects. Changing it is already a breaking change under our policy. The protocol doesn't add a compatibility promise; it makes the documented one machine-readable.This needs an argparse maintainer to pick a direction. @serhiy-storchaka / @savannahostrowski, do you prefer (a) protocols in
argparse.pyor (b) anargparse.typessubmodule? I'll rework the PR to match, and I'll do theargparse.typespackaging if that's the call.picnixz commentedon May 31, 2026
I know. And this is why I'm against having it in argparse.py.
Also, you forgot one solution: (c) do nothing. I'm +0.5 on adding a protocol but if a maintainer doesn'tt want to, we won't add it.
gaborbernat commentedon May 31, 2026
True, but was hoping for a solution here 😊 and do nothing doesn't really solves the problem in my book, forcing users to import private types...
savannahostrowski commentedon Jun 3, 2026
TBH, all of this sounds pretty heavy for what's effectively "align the module with how people already use the API". I feel like a much more pragmatic approach would just be to make
ArgumentGroup/MutuallyExclusiveGrouppublic and keep the_-prefixed names as backwards compat aliases. It's ~2 lines of code in the module, far less maintenance burden and ceremony, and no second copy of the contract to keep in sync.I could maybe justify the protocol if we genuinely believed we'd someday fully rearchitect these classes but 16 years of status quo says we won't 🙂. And even if we did, we'd have to handle the old names in the same way either way (plenty of code already imports the
_-prefixed ones), so the benefit is small IMO. The protocol's only real edge is letting us swap the class identity, which isn't a problem we actually have.To be clear, if we went this route, I'd still suggest only documenting the already-documented public methods as the supported API. The constructor and inherited internals stay implementation details, so we're not committing to anything the docs didn't already promise. Also, there's prior art for this approach. Here are some examples of some stdlib modules promoting a private class to public while keeping the
_-prefixed name as a backwards compat alias (just a quick search!):gaborbernat commentedon Jun 3, 2026
I'm happy to go down that path. The only reason I implemented here the protocol approach is because previously on this issue I got pushed back when I suggested to make it public. So if you sign off on making it public, I'm happy to go down that path. It's what I was initially championing for too.
gaborbernat commentedon Sep 23, 2026
@savannahostrowski so what did we decide in, can I open a new PR to expose these publicly then?