Python argparse custom action and custom type

Baoshan Gu
2 min readJun 18, 2021

--

Package argparse is widely used to parse arguments. It fits the needs nicely in most cases. Sometimes we might want to customize it. In this case, argparse provides custom action and custom type feature.

Custom type without additional arguments

Let us say you want a command line argument that it is a directory with the writable permission. With custom type, it can be done as this:

# test_type.py
import os
import argparse
def ExistingWritableDir(dir_name):
if os.path.exists(dir_name) and os.path.isdir(dir_name) and os.access(dir_name, os.W_OK):
return dir_name # you can return an updated value if needed.
else:
raise argparse.ArgumentTypeError(f"{dir_name} is not an existing writable dir")
parser = argparse.ArgumentParser(description="Custom type example")
parser.add_argument('--writable_dir_type', type=ExistingWritableDir)
args = parser.parse_args()
print(f"writable_dir_type: {args.writable_dir_type}")

Running the script you will get something like the following if tt1 is a writable directory:

$ python test_type.py --writable_dir_type tt1
writable_dir_type: tt1

Otherwise, you will get:

$ python test_type.py --writable_dir_type non-dir
usage: test_type.py [-h] [--writable_dir_type WRITABLE_DIR_TYPE]
test_type.py: error: argument --writable_dir_type: non-dir is not an existing writable dir

Custom type with additional arguments

For example, you’d like a script parameter following certain pattern. The pattern is an additional argument passed to the custom type.

import os
import re
class PatternedName(object):
def __init__(self, pattern):
self._pattern = pattern
def __call__(self, name):
if re.match(self._pattern, name):
return name
else:
raise argparse.ArgumentTypeError(f"{name} does not follow the expected pattern.")
parser = argparse.ArgumentParser(description="Custom type example")
parser.add_argument('--patterned_name', type=PatternedName(r'^[a-f0-9]+$'))
args = parser.parse_args()

Custom action

We can implement the same feature with custom action.

import os
import re
import argparse
def check_existing_writable_dir(parser, dir_name):
if os.path.exists(dir_name) and os.path.isdir(dir_name) and os.access(dir_name, os.W_OK):
return dir_name # you can return an updated value if needed.
else:
parser.error(f"{dir_name} is not an existing writable dir")
def check_patterned_name(pattern): # you can also implment this as a class similar to PatternedName
def internal_func(parser, name):
if re.match(pattern, name):
return name
else:
parser.error(f"{name} does not follow the expected pattern.")
return internal_func
class CustomAction(argparse.Action):
def __init__(self, check_func, *args, **kwargs):
"""
argparse custom action.
:param check_func: callable to do the real check.
"""
self._check_func = check_func
super(CustomAction, self).__init__(*args, **kwargs)
def __call__(self, parser, namespace, values, option_string):
if isinstance(values, list):
values = [self._check_func(parser, v) for v in values]
else:
values = self._check_func(parser, values)
setattr(namespace, self.dest, values)
parser = argparse.ArgumentParser(description="Custom action example")
parser.add_argument('--writable_dir_action', action=CustomAction, check_func=check_existing_writable_dir)
parser.add_argument('--patterned_name_action', action=CustomAction, check_func=check_patterned_name(r'^[a-f0-9]+'))
args = parser.parse_args()
print(f"""
writable_dir_action: {args.writable_dir_action},
patterned_name_action: {args.patterned_name_action}""")

If the command line argument is a list, simply pass nargs='+' to add_argument statement.

Happy coding!

--

--