Skip to content

argparse exits on error even when exit_on_error=False #103498

Description

@sfermigier

Bug report

When ArgumentParser encounters certain errors, it calls self.error(...) which exits, even when exit_on_error is set to False.

This prevents catching the exception and writing a custom error message.

import argparse

parser = argparse.ArgumentParser(exit_on_error=False)
parser.add_argument('filename')

try:
    args = parser.parse_args([])
except Exception:
    print("Bad argument, here's what you should do instead...")

Test on Python 3.11.3:

python argparse-bug/repro.py
usage: repro.py [-h] filename
repro.py: error: the following arguments are required: filename

Linked PRs

Activity

  1. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Apr 13, 2023
  2. sunmy2019 commented on Apr 13, 2023

    @sunmy2019
    Member

    The feature was added at #15362
    It looks like the author forgot about this case. A direct modification is acceptable here.

  3. added a commit that references this issue on Apr 13, 2023
  4. JustBeYou commented on Apr 13, 2023

    @JustBeYou

    Hi, new contributor here 👋

    I written a quick fix which should be generic (instead of guarding all calls to self.error, I added the guard in the method itself). But this way, throwing custom exceptions is not possible. (for example in the PR implementing the feature there was a test expecting a argparse.ArgumentError exception). Is this acceptable or is throwing custom exceptions desired?

    If it is desired, we could do the following without breaking the existing API:

    1. def error(self, exception_or_message: Union[str, Exception]) - allow passing both messages and exceptions
    2. def error(self, message: str, exception_class=None) - allow passing exception class information in a separate argument

    Personally, I find the first one the simplest and requires little changes to the code. I would be glad if any maintainer could give an opinion.

  5. gaogaotiantian commented on Apr 13, 2023

    @gaogaotiantian
    Member

    As far as I can tell, all the self.error() usage is reporting an issue with argument parsing(well, its the argparse library of course). In _parse_known_args there are mixed usage of ArgumentError and self.error() - which is pretty bad.

    Doing all the error handling in self.error() seems reasonable, but I don't think we need to change the signature in self.error(). Just raise ArgumentError with the message if self.exit_on_error is False - it's easier for user to catch it anyway.

    We also need to replace all the raise ArgumentError with self.error() to keep the code clean.

  6. added 3 commits that reference this issue on Apr 13, 2023
  7. gaogaotiantian commented on Apr 13, 2023

    @gaogaotiantian
    Member

    Hmm, I did not realize that ArgumentError actually takes an action, that was my fault. I still think the current implementation is better, but there might be a backward compatibility issue involved. It's clearly stated that the user can override error() function. If you added an extra argument and used it, the user's current code might break. I think we need some opinions from the component owner.

  8. JustBeYou commented on Apr 13, 2023

    @JustBeYou

    No, I did not reply to the email directly, I just wanted to edit the comment because I noticed the compatibility issue after I written it, but I deleted it instead. 😅

    Anyway, I followed your advice, but stumbled upon this action parameter that breaks the API for classes that inherit ArgumentParser. We could avoid this by using **kwargs, but it is not the cleanest way to solve it.

  9. gaogaotiantian commented on Apr 13, 2023

    @gaogaotiantian
    Member

    Taking at the look at the code, when this feature was intruduced, it was not very thoroughly tested/reviewed. Basically all the self.error() usage need to be vetted - by nature it contradicts this feature. So this feature either has to live in self.error(), or all the current self.error() usage need to be protected by checking this option.

  10. jacobtylerwalls commented on Apr 15, 2023

    @jacobtylerwalls
    Contributor

    For triage's sake, I think this is a duplicate of #85427, which has a PR waiting for review at #30832.

    But this way, throwing custom exceptions is not possible. (for example in the PR implementing the feature there was a test expecting a argparse.ArgumentError exception). Is this acceptable or is throwing custom exceptions desired?

    There's an even older duplicate with some discussion on this point that shaped the choices I made in #30832, see #30832 (comment).

  11. 2 remaining items

  12. olgarithms commented on Apr 24, 2023

    @olgarithms
    Contributor

    I also came across the same issue where creating an ArgumentParser with exit_on_error=False and passing the wrong type of argument would raise an argparse.ArgumentError, but passing an unknown argument would exit the task.

    import argparse
    
    parser = argparse.ArgumentParser(exit_on_error=False)
    parser.add_argument("--integers", type=int)
    parser.parse_args(["--integers", "a"]).  
    > argparse.ArgumentError: argument --integers: invalid int value: 'a'
    parser.parse_args(["--unknown", "a"])
    > usage: olga.py [-h] [--integers INTEGERS]
    > olga.py: error: unrecognized arguments: --unknown a
  13. hpaulj commented on Apr 24, 2023

    @hpaulj

    Yes, as currently implemented exit_on_error only controls errors that raise a argparse.ArgumentError. The unrecognized arguments exit does not use ArgumentError (or an error Exception class) , and so cannot be caught in the same way.

    A more comprehensive way of changing error handling is to customize either the exit or `error methods:

    https://docs.python.org/3/library/argparse.html#exiting-methods

  14. gaogaotiantian commented on Apr 24, 2023

    @gaogaotiantian
    Member

    I think we are basically at a dead loop here - we want to keep the backward compatibility so we don't breaking anyone's existing code, but the current behavior is simply wrong. It's impossible to "fix" the behavior without breaking existing code.

    The introduction of exit_on_error does not live well with all the sys.exit() usage in argparse. There could be a lot of similar traps unless we patch all the sys.exit()s (self.error()).

    Anyone who had to make a decision about fixing this would carry plenty of pressure for breaking people's code and I believe according to @sobolevn none of the core devs are the experts of argparse now. I'm curious if/how we could proceed on this.

  15. JustBeYou commented on May 4, 2023

    @JustBeYou

    I took a deeper dive into the problem and I may have a solution. I think the main goals for the fix are:

    • raise exceptions for all kinds of errors ArgumentParser could encounter
    • prevent future bugs related to exit_on_error=False by calling .exit() indirectly by accident
    • stay as backward compatible as possible

    First, to make the error handling consistent, I propose that we convert all calls to self.error() to raising exceptions (in case ArgumentError does not fit properly in some cases, we can think of a new public exception type, but I think we won't need that).

    Second, we make all public methods of ArgumentParser (there are few) to handle exit_on_error in an obvious way. If exit_on_error=False, we allow exception to pass through, otherwise we pass their string representation to self.error which will do its thing and finally exit.

    I think that if we use self.error only in simple try-catch blocks in the public methods, it would be easy enough to make the right behaviour happen and avoid future mistakes.

    I will update the pull request to reflect what I was talking about and I'm looking for suggestions/opinions. Maybe we succeed to move this forward.

    Below is a list of places where self.exit/self.error are used.

    Calls to ArgumentParser.exit which are justified and probably do not need to be changed:

    _HelpAction.__call__
    _VersionAction.__call__
    

    Calls to ArgumentParser.error that should be converted to raising exceptions:

    ArgumentParser.add_subparsers
    ArgumentParser.parse_args
    ArgumentParser.parse_known_args
    ArgumentParser.parse_intermixed_args
    
    ArgumentParser._parse_known_args
    ArgumentParser._read_args_from_files
    ArgumentParser._parse_optional
    ArgumentParser._get_option_tuples
    
  16. gaogaotiantian commented on May 4, 2023

    @gaogaotiantian
    Member

    I took a deeper dive into the problem and I may have a solution. I think the main goals for the fix are:

    • raise exceptions for all kinds of errors ArgumentParser could encounter
    • prevent future bugs related to exit_on_error=False by calling .exit() indirectly by accident
    • stay as backward compatible as possible

    I'm sure there are ways to make the code better, but I believe the major issue here is not how, it is backward compatibility. For example, what if the user are subclassing ArgumentParser and using self.error() in their code? Your change would break the behavior.

    Like I said above, the real issue here is the library was used too much by the users and all the implementation details are considered "documented feature". We either keep the old "wrong" behavior, or break users existing code.

    The hard part is to make the decision - and that's where we are stuck I believe. We need core devs to support a reform, even if that means breaking some of the user code. Without that, I don't believe there would be an implemetation feasible.

  17. alonbl commented on Nov 5, 2023

    @alonbl

    Hi,

    The exit_on_error=False is also used to ignore undefined arguments and return args collection of what argparse succeeded to parse. It should not raise exception as then the parse will not be able to return its value.

    Regards,

  18. hpaulj commented on Nov 5, 2023

    @hpaulj
  19. hpaulj commented on Nov 6, 2023

    @hpaulj
  20. androidkh commented on Dec 11, 2023

    @androidkh

    Hi all,

    the issue still exists for me in version 1.4.0.
    Error stack trace:
    ...

    argparse.py:1829: in parse_args
        self.error(msg % ' '.join(argv))
    argparse.py:2587: in error
        self.exit(2, _('%(prog)s: error: %(message)s\n') % args)
    argparse.py:2574: in exit
        _sys.exit(status)
    

    As I can see, on line 1857 it actually tries to verify 'if self.exit_on_error' and then use try..catch but in fact it doesn't reach that block as at line 1827 it goes to self.error that later leads to exit

  21. hpaulj commented on Dec 11, 2023

    @hpaulj
  22. olgarithms commented on May 20, 2024

    @olgarithms
    Contributor

    @ericvsmith looking into this!

  23. serhiy-storchaka commented on Jun 26, 2024

    @serhiy-storchaka
    Member

    I wrote #121056 before discovering this issue. It fixes the issue in the way opposite to #103519: internal error() calls are replaced with raising ArgumentError. It is then caught and passed to error() at the higher level. Basically, it implements the idea proposed in #103498 (comment).

    We now have 3 duplicate issues with 4 open PRs (and one incomplete PR was already merged). It is time to finish this.

  24. moved this from Bugs to Doc issues in Argparse issueson Jun 28, 2024
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/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions