Skip to content

Make _MutuallyExclusiveGroup a public API in argparse #144812

Open

Description

@gaborbernat

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:

group = parser.add_argument_group("selection")
add_to: ArgumentParser = group.add_mutually_exclusive_group(required=False)

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.

Linked PRs

Activity

  1. added
    type-featureA feature request or enhancement
    stdlibStandard Library Python modules in the Lib/ directory
    on Feb 14, 2026
  2. serhiy-storchaka commented on Feb 15, 2026

    @serhiy-storchaka
    Member

    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.

  3. gaborbernat commented on Feb 15, 2026

    @gaborbernat
    ContributorAuthor

    Create a protocol, and use it.

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

  4. picnixz commented on Feb 16, 2026

    @picnixz
    Member

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

  5. added
    pendingThe issue will be closed if no feedback is provided
    on Feb 16, 2026
  6. mpkocher commented on Feb 17, 2026

    @mpkocher
    Contributor

    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.

  7. mpkocher commented on Feb 17, 2026

    @mpkocher
    Contributor

    Or just use non-public classes if you are not afraid that your code can break with future Python versions.

    https://github.com/search?q=%22argparse._MutuallyExclusiveGroup%22+language%3APython&type=code

  8. gaborbernat commented on May 30, 2026

    @gaborbernat
    ContributorAuthor

    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.

  9. picnixz commented on May 30, 2026

    @picnixz
    Member

    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.

  10. picnixz commented on May 30, 2026

    @picnixz
    Member

    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.

  11. gaborbernat commented on May 30, 2026

    @gaborbernat
    ContributorAuthor

    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.

  12. picnixz commented on May 31, 2026

    @picnixz
    Member

    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.

  13. picnixz commented on May 31, 2026

    @picnixz
    Member

    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)

  14. gaborbernat commented on May 31, 2026

    @gaborbernat
    ContributorAuthor

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

  15. picnixz commented on May 31, 2026

    @picnixz
    Member

    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.

  16. gaborbernat commented on May 31, 2026

    @gaborbernat
    ContributorAuthor

    Also, you forgot one solution: (c) do nothing.

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

  17. savannahostrowski commented on Jun 3, 2026

    @savannahostrowski
    Member

    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!):

  18. gaborbernat commented on Jun 3, 2026

    @gaborbernat
    ContributorAuthor

    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.

  19. removed
    pendingThe issue will be closed if no feedback is provided
    on Jul 18, 2026
  20. gaborbernat commented on Sep 23, 2026

    @gaborbernat
    ContributorAuthor

    @savannahostrowski so what did we decide in, can I open a new PR to expose these publicly then?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytopic-typingtype-featureA feature request or enhancement

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions