I only just learned about this today after all these years and thought you might like it too.
The trick is to conveniently turn an argparse.Namespace
into keyword arguments that you can send to a function. This is the old/wrong way I've been doing it for years:
# THE OLD WAY
def main(things, option_a, option_n):
print(locals()) # Debugging
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("things", help="Bla bla", nargs="*")
parser.add_argument("-o", "--option-a", help="Bla bla", default="Op A")
parser.add_argument("-n", "--option-n", help="Ble ble", default="Op N")
args = parser.parse_args()
main(
things=args.things,
option_a=args.option_a,
option_n=args.option_n
)
That works but the tedious thing is to have to have spell out every single argument, twice!, when sending the argparse
Namespace into the function. Here's the much moar betterest way:
# THE NEW WAY
def main(things, option_a, option_n):
print(locals()) # Debugging
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("things", help="Bla bla", nargs="*")
parser.add_argument("-o", "--option-a", help="Bla bla", default="Op A")
parser.add_argument("-n", "--option-n", help="Ble ble", default="Op N")
args = parser.parse_args()
# The only difference and the magic sauce...
main(**vars(args))
What's neat about this is that you don't have to type up every argument defined in the parser to the get it as arguments into a function. And as a bonus, Python will name match keyword arguments to arguments so the order doesn't matter.
Caveat!
This "trick" assumes that the arguments in the parser match the arguments in the function. So if the main()
function takes an argument called foo_bar
you have to have an argument in the parser called --foo-bar
.
Comments
One could do so.
Imho, the post reaches a flawed solution. I personally just keep the Namespace object as-is and pass it as an argument. This keeps everything together, reduces argument count and potential for mixing stuff. This also avoids the unwanted duplication your solution tries to avoid.
Here's what I always do:
```python
import argparse
import typing
class Namespace(typing.NamedTuple):
"""Mock Namespace used for type hinting purposes. Keep in sync with the argument parser defined in parse_args"""
option_a: bool
option_b: int
def parse_args() -> Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("-o", "--option-a", action="store_true")
parser.add_argument("-n", "--option-b", type=int, default=0)
return parser.parse_args()
def main(args: Namespace):
if args.option_a:
print(args.option_b)
some_logic(args)
def some_logic(args: Namespace):
pass
if __name__ == "__main__":
args = parse_args()
main(args)
```
With that approach, the IDE/linter can do static type checking, and complains when assigning to the namespace, or when types don't match, or when accessed arguments don't exist.
You have to write a unit test though, that ensures that the mock Namespace is in sync with the actual one created by the argument parser. But that's easy enough, just call parse_args() in a test and compare the result keys/value types with the Namespace.__annotations__ dict.
One thing to keep in mind is that the mock Namespace is not the actual Namespace class. So you cannot do checks like `isinstance(args, Namespace)`. But those shouldn't be done in the first place, anyway.