qargparse
qargparse is a Python utility library that extends the standard argparse module with powerful, task-oriented command-line parsing. It is designed to help developers build structured CLI tools that track parameter history, execute tasks, and simplify dynamic argument management.
qargparse primary focus is as a flexible argument and task management system that makes building complex CLIs cleaner and more maintainable.
Features
- ✅ Ordered Argument History: Track each call of a parameter in the order it was used.
- ✅ Repeated Arguments: Arguments can be repeated and grouped.
- ✅ Task-Based CLI: Define tasks that execute specific logic based on accumulated arguments.
- ✅ Custom Actions: Includes
q,q_true,q_false, andtaskactions for flexible argument handling. - ✅ Custom Types: Includes
argsandkwargstypes to coercestr,int,float,bool,None,listanddicttypes. - ✅ Integration Ready: Works with standard
argparsewhile providing advanced extensions. - ✅ API to CLI conversion: Python classes can be converted automatically into CLIs
Installation
pip install qargparse
Or install from source:
git clone https://github.com/dwish/qargparse.git
cd qargparse
pip install .
Quick Start
- Create a Parser
from qargparse import ArgumentParser
parser = ArgumentParser()
- Add Parameters
qargparse extends standard argparse with task-friendly actions:
parser.add_argument("--x") # Regular argparse argument
parser.add_argument("--a", action='q') # Stores each call in order
parser.add_argument("--b", action='q_true')
parser.add_argument("--c", action='q', default='foo')
parser.add_argument("--run_1", action='task') # Defines a task to execute
parser.add_argument("--run_2", action='task') # Defines a task to execute
- Parse Arguments
opts, tasks = parser.parse_args([
"--x", "foo",
"--a", "1", "--b", "--run_1",
"--a", "2", "--c", "bar", "--run_1",
"--b", "--run_2"])
print(opts) # Namespace of parsed arguments
"""
Namespace(x='foo')
"""
print(tasks) # List of tasks with their corresponding arguments and default values:
"""
TasksKwargs(tasks=[('run_1', {'a': '1', 'b': True}), ('run_1', {'a': '2', 'c': 'bar'}), ('run_2', {'b': True})], defaults={'c': {'': 'foo'}})
"""
- Task-Based Execution
You can define tasks that consume the accumulated arguments:
obj = ... # Create an instance of your object of interest!
for task in tasks:
match task.name:
case "run_1":
obj = obj.my_method_1(**task.kwargs)
case "run_2":
task = task.set_defaults('c') # You can update missing kwargs with defaults
obj = obj.my_method_2(**task.kwargs)
This allows multiple invocations of the same task with different arguments and/or invocation of different tasks in a single CLI call.
What happened?
action="q"queues arguments in order.action="task"defines a task boundary.- Each
--runfinalizes a task with the accumulated queued arguments.
This allows multiple logical executions in a single CLI call.
Parameter Deduplication
What if you have the same parameter for different tasks?
parser = ArgumentParser()
parser.add_argument("--a", action='q', help="Run 1 argument...")
parser.add_argument("--run_1", action='task')
parser.add_argument("--a", action='q', help="... but also Run 2 argument!")
parser.add_argument("--run_2", action='task')
argparse do not allow to set the same option-string multiple times.
Regroup
A straightforward solution is to define the duplicated arguments only once:
parser = ArgumentParser()
parser.add_argument("--a", action='q', default=1, help="Run 1 and 2 argument")
parser.add_argument("--run_1", action='task')
parser.add_argument("--run_2", action='task')
opts, tasks = parser.parse_args(["--run_1", "--run_2"])
print(tasks)
"""
TasksKwargs(tasks=[('run_1', {}), ('run_2', {})], defaults={'a': {'': 1}})
"""
This format constraint the default value and help of a to be the same in
the tasks invokated by run_1 and run_2, which may not be the wanted
behavior!
Alias
An alternative, to set specific default value and help message to duplicated
parameters accross tasks, is to use aliases with the custom alias
parameter in add_argument:
parser = ArgumentParser()
parser.add_argument("--global", action='q', default='foo', help="Global queue parameter")
parser.add_argument("--a", action='q', default=1, help="Run 1 help", alias="run_1")
parser.add_argument("--run_1", action='task')
parser.add_argument("--a", action='q', default=2, help="Run 2 help", alias="run_2")
parser.add_argument("--run_2", action='task')
opts, tasks = parser.parse_args(["--run_1", "--run_2"])
print(tasks)
"""
TasksKwargs(tasks=[('run_1', {}), ('run_2', {})], defaults={'global': {'': 'foo'}, 'a': {'run_1': 1, 'run_2': 2}})
"""
If the alias is identical to a task name, it will be automatically added to the task kwargs if missing:
print(tasks[0])
"""
TaskKwargs(task='run_1', kwargs={'a': 1}, defaults={'global': {'': 'foo'}})
"""
print(tasks[1])
"""
TaskKwargs(task='run_2', kwargs={'a': 2}, defaults={'global': {'': 'foo'}})
"""
Kwargs Defaults
You can add any defined default value to a task kwargs with set_defaults.
Good-Practice propositions
In order to keep parameters definition organized and clear, we propose the following organisation:
- Regular argparse parameters (non-q* actions) with
add_argument - Global q* parameters (without alias) with
add_argument - Tasks, with the following organization for each task:
- A group corresponding to the task with
add_argument_group - Task-specific parameters with
add_argumentand alias - Task parameter with
add_argumentandaction=task
- A group corresponding to the task with
This proposed organization can be automatically achieve with add_arguments:
parser = ArgumentParser()
parser.add_arguments(
regular=[
("--foo", dict(default="foo", help="Regular action.")),
("--bar", dict(default="bar", help="Regular action."))
],
q_global=[
("--a", dict(default='x', help="Global queue action."), dict(run_1=dict(default='a'), run_2=dict(help="Custom global queue action help for run_2"), run_3=dict(default='c', help="--a parameter of run_3"))),
("--b", dict(default=0, help="Global queue action.")),
("--c", dict(action='q_true', help="Global queue action."))
],
tasks={
'run_1': [
("--d", dict(help="Run 1 specific action.")),
("--e", dict(default='A', help="Run 1 specific action."))
],
'run_2': (
[
("--f", dict(help="Run 2 specific action.")),
("--g", dict(default='B', help="Run 2 specific action."))
],
dict(title="Custom title for Run 2.", description="Run 2 does a lots of things!")
),
'run_3': (
[],
dict(title="Custom title for Run 3.", description="Run 3 have a default value for --a!")
)
}
)
opts, tasks = parser.parse_args(["--run_1", "--run_2"])
print(parser.format_help())
"""
usage: [-h] [--foo FOO] [--bar BAR] [--a A] [--b B] [--c] [--d D] [--e E] [-ALIAS_'run_1'_OF:--a A] [--run_1] [--f F]
[--g G] [-ALIAS_'run_2'_OF:--a A] [--run_2] [--run_3]
options:
-h, --help show this help message and exit
--foo FOO Regular action.
--bar BAR Regular action.
--a A Global queue action.
--b B Global queue action.
--c Global queue action.
Run 1:
Specific parameters of: run_1()
--d D Run 1 specific action.
--e E Run 1 specific action.
-ALIAS_'run_1'_OF:--a A
--run_1 Launch 'run_1' task.
Custom title for Run 2.:
Run 2 does a lots of things!
--f F Run 2 specific action.
--g G Run 2 specific action.
-ALIAS_'run_2'_OF:--a A
Custom global queue action help for run_2
--run_2 Launch 'run_2' task.
Custom title for Run 3.:
Run 3 have a default value for --a!
--run_3 Launch 'run_3' task.
"""
API-to-CLI Conversion
qargparse includes utilities to generate CLI templates from Python classes:
from qargparse import api_to_cli_template
api_to_cli_template(<api>, dedup_kwargs_strategy="regroup", output="template.py")
# or
api_to_cli_template(<api>, dedup_kwargs_strategy="alias", output="template.py")
This generates a Python template that builds a qargparse.ArgumentParser ready to parse tasks and parameters for the class, as well as the class methods processing in the CLI.
Note: The API-to-CLI functionality is intended as a helper utility;
qargparsecan be used fully manually for maximum flexibility.
Example CLI
Random
import random
from qargparse import api_to_cli_template
print(api_to_cli_template(random, only_methods=['randint', 'randrange']))
"""
# TEMPLATE: the following is a template, investigate/update it!
import random
import qargparse
parser = qargparse.ArgumentParser()
# Regular argparse arguments
## Add regular argparse arguments here
# Global queue arguments
parser = parser.add_argument("args", dest="args", action="q", nargs="*", dtype="args")
parser = parser.add_argument("--kwargs", dest="kwargs", action="q", nargs="*", dtype="kwargs")
# Method-specific queue arguments & tasks
PARSER_TASK_RANDINT = parser.add_argument_group(title="Randint", description="")
PARSER_TASK_RANDINT = parser.add_argument("--a", dest="a", action="q", dtype="args", help="Required. ")
PARSER_TASK_RANDINT = parser.add_argument("--b", dest="b", action="q", dtype="args", help="Required. ")
PARSER_TASK_RANDINT = parser.add_argument("--randint", dest="randint", action="task", help="Launch 'randint' task: Return random integer in range [a, b], including both end points. (<class 'inspect._empty'>)")
PARSER_TASK_RANDRANGE = parser.add_argument_group(title="Randrange", description="")
PARSER_TASK_RANDRANGE = parser.add_argument("--start", dest="start", action="q", dtype="args", help="Required. ")
PARSER_TASK_RANDRANGE = parser.add_argument("--stop", dest="stop", action="q", dtype="args", help="")
PARSER_TASK_RANDRANGE = parser.add_argument("--step", dest="step", action="q", dtype="args", default=1, help="")
PARSER_TASK_RANDRANGE = parser.add_argument("--randrange", dest="randrange", action="task", help="Launch 'randrange' task: Choose a random item from range(stop) or range(start, stop[, step]). (<class 'inspect._empty'>)")
# Parse the command-line
opts, tasks = parser.parse_args()
# Regular argparse argument processing
## Add your regular argparse processing here
# Tasks processing
for task in tasks:
match task.name:
case "randint":
# TEMPLATE: the following is a template, investigate/update it!
result = random.randint(**task.kwargs)
case "randrange":
# TEMPLATE: the following is a template, investigate/update it!
result = random.randrange(**task.kwargs)
"""
Pandas DataFrame
import pandas as pd
from qargparse import api_to_cli_template
print(api_to_cli_template(pd.DataFrame, only_methods=['merge', 'take']))
"""
# TEMPLATE: the following is a template, investigate/update it!
from pandas import DataFrame
import qargparse
parser = qargparse.ArgumentParser()
# Regular argparse arguments
## Add regular argparse arguments here
# Global queue arguments
parser = parser.add_argument("args", dest="args", action="q", nargs="*", dtype="args")
parser = parser.add_argument("--kwargs", dest="kwargs", action="q", nargs="*", dtype="kwargs")
# Method-specific queue arguments & tasks
PARSER_TASK_MERGE = parser.add_argument_group(title="Merge", description="")
PARSER_TASK_MERGE = parser.add_argument("--right", dest="right", action="q", dtype="args", help="Required. Object to merge with. (DataFrame or named Series) ")
PARSER_TASK_MERGE = parser.add_argument("--how", dest="how", action="q", dtype="args", default="inner", help="default 'inner'\nType of merge to be performed.\n\n* left: use only keys from left frame, similar to a SQL left outer join;\n preserve key order.\n* right: use only keys from right frame, similar to a SQL right outer join;\n preserve key order.\n* outer: use union of keys from both frames, similar to a SQL full outer\n join; sort keys lexicographically.\n* inner: use intersection of keys from both frames, similar to a SQL inner\n join; preserve the order of the left keys.\n* cross: creates the cartesian product from both frames, preserves the order\n of the left keys.\n* left_anti: use only keys from left frame that are not in right frame, similar\n to SQL left anti join; preserve key order.\n\n .. versionadded:: 3.0\n* right_anti: use only keys from right frame that are not in left frame, similar\n to SQL right anti join; preserve key order.\n\n .. versionadded:: 3.0 ('left', 'right', 'outer', 'inner', 'cross', 'left_anti', 'right_anti') ")
PARSER_TASK_MERGE = parser.add_argument("--on", dest="on", action="q", dtype="args", help="Column or index level names to join on. These must be found in both\nDataFrames. If `on` is None and not merging on indexes then this defaults\nto the intersection of the columns in both DataFrames. (Hashable or a sequence of the previous) ")
PARSER_TASK_MERGE = parser.add_argument("--left_on", dest="left_on", action="q", dtype="args", help="Column or index level names to join on in the left DataFrame. Can also\nbe an array or list of arrays of the length of the left DataFrame.\nThese arrays are treated as if they are columns. (Hashable or a sequence of the previous, or array-like) ")
PARSER_TASK_MERGE = parser.add_argument("--right_on", dest="right_on", action="q", dtype="args", help="Column or index level names to join on in the right DataFrame. Can also\nbe an array or list of arrays of the length of the right DataFrame.\nThese arrays are treated as if they are columns. (Hashable or a sequence of the previous, or array-like) ")
PARSER_TASK_MERGE = parser.add_argument("--left_index", dest="left_index", action="q_false", dtype="args", default=False, help="Use the index from the left DataFrame as the join key(s). If it is a\nMultiIndex, the number of keys in the other DataFrame (either the index\nor a number of columns) must match the number of levels. (bool) ")
PARSER_TASK_MERGE = parser.add_argument("--right_index", dest="right_index", action="q_false", dtype="args", default=False, help="Use the index from the right DataFrame as the join key. Same caveats as\nleft_index. (bool) ")
PARSER_TASK_MERGE = parser.add_argument("--sort", dest="sort", action="q_false", dtype="args", default=False, help="Sort the join keys lexicographically in the result DataFrame. If False,\nthe order of the join keys depends on the join type (how keyword). (bool) ")
PARSER_TASK_MERGE = parser.add_argument("--suffixes", dest="suffixes", action="q", dtype="args", help="A length-2 sequence where each element is optionally a string\nindicating the suffix to add to overlapping column names in\n`left` and `right` respectively. Pass a value of `None` instead\nof a string to indicate that the column name from `left` or\n`right` should be left as-is, with no suffix. At least one of the\nvalues must not be None. (list-like) ")
PARSER_TASK_MERGE = parser.add_argument("--copy", dest="copy", action="q", dtype="args", help="This keyword is now ignored; changing its value will have no\nimpact on the method.\n\n.. deprecated:: 3.0.0\n\n This keyword is ignored and will be removed in pandas 4.0. Since\n pandas 3.0, this method always returns a new object using a lazy\n copy mechanism that defers copies until necessary\n (Copy-on-Write). See the `user guide on Copy-on-Write\n <https://pandas.pydata.org/docs/dev/user_guide/copy_on_write.html>`__\n for more details. (bool) ")
PARSER_TASK_MERGE = parser.add_argument("--indicator", dest="indicator", action="q", dtype="args", default=False, help="If True, adds a column to the output DataFrame called \\\"_merge\\\" with\ninformation on the source of each row. The column can be given a different\nname by providing a string argument. The column will have a Categorical\ntype with the value of \\\"left_only\\\" for observations whose merge key only\nappears in the left DataFrame, \\\"right_only\\\" for observations\nwhose merge key only appears in the right DataFrame, and \\\"both\\\"\nif the observation's merge key is found in both DataFrames. (bool or str) ")
PARSER_TASK_MERGE = parser.add_argument("--validate", dest="validate", action="q", dtype="args", help="If specified, checks if merge is of specified type.\n\n* \\\"one_to_one\\\" or \\\"1:1\\\": check if merge keys are unique in both\n left and right datasets.\n* \\\"one_to_many\\\" or \\\"1:m\\\": check if merge keys are unique in left\n dataset.\n* \\\"many_to_one\\\" or \\\"m:1\\\": check if merge keys are unique in right\n dataset.\n* \\\"many_to_many\\\" or \\\"m:m\\\": allowed, but does not result in checks. (str) ")
PARSER_TASK_MERGE = parser.add_argument("--merge", dest="merge", action="task", help="Launch 'merge' task: Merge DataFrame or named Series objects with a database-style join. (DataFrame)")
PARSER_TASK_TAKE = parser.add_argument_group(title="Take", description="")
PARSER_TASK_TAKE = parser.add_argument("--indices", dest="indices", action="q", dtype="args", help="Required. An array of ints indicating which positions to take. (array-like) ")
PARSER_TASK_TAKE = parser.add_argument("--axis", dest="axis", action="q", dtype="args", default=0, help="The axis on which to select elements. ``0`` means that we are\nselecting rows, ``1`` means that we are selecting columns.\nFor `Series` this parameter is unused and defaults to 0. (0 or 'index', 1 or 'columns') ")
PARSER_TASK_TAKE = parser.add_argument("--take", dest="take", action="task", help="Launch 'take' task: Return the elements in the given *positional* indices along an axis. (Self)")
# Parse the command-line
opts, tasks = parser.parse_args()
# TEMPLATE: the following is a template, investigate/update it!
obj = DataFrame(...) # Create your instance
# Regular argparse argument processing
## Add your regular argparse processing here
# Tasks processing
for task in tasks:
match task.name:
case "merge":
# TEMPLATE: the following is a template, investigate/update it!
obj = obj.merge(**task.kwargs)
case "take":
# TEMPLATE: the following is a template, investigate/update it!
obj = obj.take(**task.kwargs)
"""