DEV Community

Cover image for Python argument parsing with log level and config file
Micah Carrick
Micah Carrick

Posted on

2 2

Python argument parsing with log level and config file

Making use of parse_known_args() to set root log level and parse default values from a configuration file before parsing main args.


Suppose you're banging out a simple command-line utility in Python which has the following requirements:

  1. Root log level can be set with a command-line argument
  2. Configuration is optionally read from an external file
  3. Command-line arguments take precedence over configuration file

The argparse.ArgumentParser class includes the parse_known_args() method which allows for incrementally parsing the command-line arguments such that you can setup how subsequent arguments are parsed.

Set log level from command-line argument

The root logger must be configured before there is any output. If the argument parsing is going to be doing work that could or should produce log messages but you want the log level to be set from the arguments being parsed, use parse_known_args() to parse and set the log level first. Then proceed with the rest of the arguments.

def main(args):
logging_argparse = ArgumentParser(prog=__file__, add_help=False)
logging_argparse.add_argument('-l', '--log-level', default='WARNING',
help='set log level')
logging_args, _ = logging_argparse.parse_known_args(args)
try:
logging.basicConfig(level=logging_args.log_level)
except ValueError:
logging.error("Invalid log level: {}".format(logging_args.log_level))
sys.exit(1)
logger = logging.getLogger(__name__)
logger.info("Log level set: {}"
.format(logging.getLevelName(logger.getEffectiveLevel())))
parsers = [logging_argparse]
main_parser = ArgumentParser(prog=__file__, parents=parsers)
main_parser.add_argument('-1', '--option1')
main_parser.add_argument('-2', '--option2')
main_args = main_parser.parse_args(args)
if __name__ == "__main__":
main(sys.argv[1:])
view raw example.py hosted with ❤ by GitHub

As an aside, I'm generally a fan of using logging config files to setup more robust logging for Python applications. However, for quick 'n simple command-line scripts I like the simplicity of this approach.

Parse defaults from configuration file

A similar technique can be used to parse a configuration filename from the command-line arguments and then use that configuration file to set the default values for the remaining arguments being parsed.

Suppose a configuration file contains:

[options]
option1=config value
option2=config value
Enter fullscreen mode Exit fullscreen mode

The way this works, is the default values for subsequent commmand-line arguments are defined in a dictionary named defaults rather than using the default keyword argument with add_argument(). Then, the parse_known_args() is used to parse the configuration filename from the command line arguments. If it is present then it reads the values out of the configuration and updates the defaults dictionary. Then those defaults are applied to the remaining command-line arguments with the set_defaults() method.

def main(args):
# parse values from a configuration file if provided and use those as the
# default values for the argparse arguments
config_argparse = ArgumentParser(prog=__file__, add_help=False)
config_argparse.add_argument('-c', '--config-file',
help='path to configuration file')
config_args, _ = config_argparse.parse_known_args(args)
defaults = {
'option1': "default value",
'option2': "default value"
}
if config_args.config_file:
logger.info("Loading configuration: {}".format(config_args.config_file))
try:
config_parser = ConfigParser()
with open(config_args.config_file) as f:
config_parser.read_file(f)
config_parser.read(config_args.config_file)
except OSError as err:
logger.error(str(err))
sys.exit(1)
defaults.update(dict(config_parser.items('options')))
# parse the program's main arguments using the dictionary of defaults and
# the previous parsers as "parent' parsers
parsers = [config_argparse]
main_parser = ArgumentParser(prog=__file__, parents=parsers)
main_parser.set_defaults(**defaults)
main_parser.add_argument('-1', '--option1')
main_parser.add_argument('-2', '--option2')
main_args = main_parser.parse_args(args)
if __name__ == "__main__":
main(sys.argv[1:])
view raw example.py hosted with ❤ by GitHub

Putting it together

See the complete example.py source code which combines both of the above techniques.

Override default log level:

$ ./example.py -l DEBUG
INFO:__main__:Log level set: DEBUG
INFO:__main__:Option 1: default value
INFO:__main__:Option 2: default value
Enter fullscreen mode Exit fullscreen mode

Read values from configuration file:

$ ./example.py -l DEBUG -c example.conf
INFO:__main__:Log level set: DEBUG
INFO:__main__:Loading configuration: example.conf
INFO:__main__:Option 1: config value
INFO:__main__:Option 2: config value
Enter fullscreen mode Exit fullscreen mode

Override values with command-line arguments:

$ ./example.py -l DEBUG -c example.conf -1 "cli value"
INFO:__main__:Log level set: DEBUG
INFO:__main__:Loading configuration: example.conf
INFO:__main__:Option 1: cli value
INFO:__main__:Option 2: config value
Enter fullscreen mode Exit fullscreen mode
Retry later

Top comments (1)

Collapse
 
catchenal profile image
Cat Chenal

Thanks for this great example, @micahcarrick!

Could you please explain why you need line 20, which seems to repeat line19.
Thanks.

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs