URL: https://docs.python.org/3/library/functions.html#vars

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

Thomas Hess

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.

Your email will never ever be published.

Related posts