Add parameter `validator` to `argparse.ArgumentParser.add_argument` or enhance `argparse` docs

Note

I made this proposal first as this issue, which was obviously the wrong way to go. Sorry for the extra noise.

Problem

Often only certain values are allowed for command line argument values. For example, you may have an argument of type int that has to be positive, but setting type=int in argparse.ArgumentParser.add_argument allows also for negative values. So even in such a simple case you have to implement your own validator rejecting negative values.

It would be nice to have an easy way to integrate a validator into argparse.ArgumentParser.add_argument.

Proposed solution

Add an optional parameter validator to argparse.ArgumentParser.add_argument. It should accept a callable with one argument of the type of the added parameter and return a boolean. If present the following should happen:

  • On argument parsing the usual type conversion and value checks should be executed
  • Then the validator callable is called with the parsed and type-converted argument
  • If the result is True nothing happens and parsing is resumed
  • If the result is False the value is rejected as invalid by raising an argparse.ArgumentError explaining the failed validation

In addition, if a default value is given it should be validated similarly when add_argument is executed.

The validator may also have a second return value containing a string explaining why a value was rejected.

Maybe it would be useful to create a special exception ArgumentValidationError derived from ArgumentError that also receives the invalid parsed value.

Example

import argparse

def validate(val: int) -> bool:
    return (val >= 10) and (val <= 100)

parser = argparse.ArgumentParser()
parser.add_argument('--foo', type=int, validator=validate, help="Set to value between 10 and 100")
args = parser.parse_args()
print(args.foo)

Now, if the program is called with --foo 5 an exception is raised pointing out the validation error.

Alternatives

In the issue @siren pointed out that such a validation can be achieved with a type function or an Action class, which is technically true.

However, creating a a type function or an Action class comes with some overhead compared to my proposal above. You have to do the type conversion yourself and eventually raise an argparse.ArgumentError exception, whose arguments are also not documented.

In addition, how to use those as validators is IMHO not obvious in the documentation. Using argparse in many projects I often scanned the documentation page and never stumbled over this possibility.

Other solution

If the proposal becomes rejected, I’d suggest to at least extend the argparse documentation page with a section about validation explaining how to properly use type or action arguments for that.

Final remarks

I’d be ready to implement the proposed validator argument or to create a draft for a documentation section about validation, whatever the final decision might be.

I’m not sure why you think the type argument isn’t sufficient. If I define

def pos(s):
    n = int(s)
    if n < 0:
        raise ValueError("Number must be positive")
    return n

then type=pos gives just as useful an error message as type=int would when an invalid integer is used. I’m not saying the error is great - really good error handling is hard! But it’s no worse than builtin types, which IMO is sufficient for the problem as you described it.

If what you actually want is better error handling than that, I think you need to more clearly explain what you’re after. Better error handling is a perfectly reasonable request, but what amounts to “better” is very subjective, and without a clear explanation of what you’re after, any discussion will probably go round in circles.

3 Likes

The main advantage would be to achieve the validation with less code, particularly because I could use a lambda expression:

..., type=int, validator=lambda x: x > 0, ...

In addition

  • I don’t have to do the type conversion myself although argparse could do it
  • argparse will still know the destination type for whatever purpose
  • argparse can produce a generic error message as it does on other parsing errors
  • it is obvious in the docs how to do validation (although this can be achieved with the suggested validation section as well)

Less code also means less opportunities to insert bugs.

The proposed validator API returning a bool can only approve/reject its input, but is not able to modify the value (e.g. "1k"1024). The existing API is clearly superior, because it returns a value or raises an error.

1 Like

I see, that’s right. But my suggestion won’t disable the existing possibility to implement a special type function for such cases. It just makes live easier when a simple validator function would suffice. Like I won’t implement an Action class if I could already achieve my goal with the other arguments of add_arguments.

In my projects in most cases a simple lambda would be sufficient without cluttering the code with lots of special type functions.

Would a higher order function work for your use-case?

def validator(cls, validator):
    def type_(s):
        rv = cls(s)
        if validator(rv): return rv
        raise ValueError(rv)
    return type_

parser = argparse.ArgumentParser()
parser.add_argument('--foo', type=validator(int, lambda x: 10 <= x <= 100), help="Set to value between 10 and 100")
args = parser.parse_args()
print(args.foo)
5 Likes

Cool, that’s an interesting solution.