Package: typed-argument-parser
Typed Argument Parser (Tap)Tap is a typed modernization of Python'sargparselibrary.Tap provides the following benefits:Static type checkingCode completionSource code navigation (e.g. go to definition and go to implementation)Seethis poster, which we presented atPyCon 2020, for a presentation of some of the relevant concepts we used to guide the development of Tap.As of version 1.8.0, Tap includestapify, which runs functions or initializes classes with arguments parsed from the command line. We show an example below.# square.pyfromtapimporttapifydefsquare(num:float)->float:returnnum**2if__name__=='__main__':print(f'The square of your number is{tapify(square)}.')Runningpython square.py --num 2will printThe square of your number is 4.0.. Please seetapifyfor more details.InstallationTap requires Python 3.8+To install Tap from PyPI run:pip install typed-argument-parserTo install Tap from source, run the following commands:git clone https://github.com/swansonk14/typed-argument-parser.git
cd typed-argument-parser
pip install -e .Table of ContentsInstallationTable of ContentsTap is Python-nativeTap featuresArgumentsTap helpConfiguring argumentsAdding special argument behaviorAdding subparsersTypesArgument processingProcessing known argsSubclassingPrintingReproducibilitySaving and loading argumentsLoading from configuration filestapifyExamplesFunctionClassDataclasstapify helpCommand line vs explicit argumentsKnown argsTap is Python-nativeTo see this, let's look at an example:"""main.py"""fromtapimportTapclassSimpleArgumentParser(Tap):name:str# Your namelanguage:str='Python'# Programming languagepackage:str='Tap'# Package namestars:int# Number of starsmax_stars:int=5# Maximum starsargs=SimpleArgumentParser().parse_args()print(f'My name is{args.name}and I give the{args.language}package 'f'{args.package}{args.stars}/{args.max_stars}stars!')You use Tap the same way you use standard argparse.>>> python main.py --name Jesse --stars 5
My name is Jesse and I give the Python package Tap 5/5 stars!The equivalent argparse code is:"""main.py"""fromargparseimportArgumentParserparser=ArgumentParser()parser.add_argument('--name',type=str,required=True,help='Your name')parser.add_argument('--language',type=str,default='Python',help='Programming language')parser.add_argument('--package',type=str,default='Tap',help='Package name')parser.add_argument('--stars',type=int,required=True,help='Number of stars')parser.add_argument('--max_stars',type=int,default=5,help='Maximum stars')args=parser.parse_args()print(f'My name is{args.name}and I give the{args.language}package 'f'{args.package}{args.stars}/{args.max_stars}stars!')The advantages of being Python-native include being able to:Overwrite convenient built-in methods (e.g.process_argsensures consistency among arguments)Add custom methodsInherit from your own template classesTap featuresNow we are going to highlight some of our favorite features and give examples of how they work in practice.ArgumentsArguments are specified as class variables defined in a subclass ofTap. Variables defined asname: typeare required arguments while variables defined asname: type = valueare not required and default to the provided value.classMyTap(Tap):required_arg:strdefault_arg:str='default value'Tap helpSingle line and/or multiline comments which appear after the argument are automatically parsed into the help string provided when runningpython main.py -h. The type and default values of arguments are also provided in the help string."""main.py"""fromtapimportTapclassMyTap(Tap):x:float# What am I?pi:float=3.14# I'm pi!"""Pi is my favorite number!"""args=MyTap().parse_args()Runningpython main.py -hresults in the following:>>> python main.py -h
usage: demo.py --x X [--pi PI] [-h]

optional arguments:
  --x X       (float, required) What am I?
  --pi PI     (float, default=3.14) I'm pi! Pi is my favorite number.
  -h, --help  show this help message and exitConfiguring argumentsTo specify behavior beyond what can be specified using arguments as class variables, override theconfiguremethod.configureprovides access to advanced argument parsing features such asadd_argumentandadd_subparser.
Since Tap is a wrapper around argparse, Tap provides all of the same functionality.
We detail these two functions below.Adding special argument behaviorIn theconfiguremethod, callself.add_argumentjust as you would use argparse'sadd_argument. For example,fromtapimportTapclassMyTap(Tap):positional_argument:strlist_of_three_things:List[str]argument_with_really_long_name:intdefconfigure(self):self.add_argument('positional_argument')self.add_argument('--list_of_three_things',nargs=3)self.add_argument('-arg','--argument_with_really_long_name')Adding subparsersTo add a subparser, override theconfiguremethod and callself.add_subparser. Optionally, to specify keyword arguments (e.g.,help) to the subparser collection, callself.add_subparsers. For example,classSubparserA(Tap):bar:int# bar helpclassSubparserB(Tap):baz:Literal['X','Y','Z']# baz helpclassArgs(Tap):foo:bool=False# foo helpdefconfigure(self):self.add_subparsers(help='sub-command help')self.add_subparser('a',SubparserA,help='a help')self.add_subparser('b',SubparserB,help='b help')TypesTap automatically handles all the following types:str,int,float,boolOptional,Optional[str],Optional[int],Optional[float],Optional[bool]List,List[str],List[int],List[float],List[bool]Set,Set[str],Set[int],Set[float],Set[bool]Tuple,Tuple[Type1,Type2,etc.],Tuple[Type,...]LiteralIf you're using Python 3.9+, then you can replaceListwithlist,Setwithset, andTuplewithtuple.Tap also supportsUnion, but this requires additional specification (seeUnionsection below).Additionally, any type that can be instantiated with a string argument can be used. For example, infrompathlibimportPathfromtapimportTapclassArgs(Tap):path:Pathargs=Args().parse_args()args.pathis aPathinstance containing the string passed in through the command line.str,int, andfloatEach is automatically parsed to their respective types, just like argparse.boolIf an argumentargis specified asarg: boolorarg: bool = False, then adding the--argflag to the command line will setargtoTrue. Ifargis specified asarg: bool = True, then adding--argsetsargtoFalse.Note that if theTapinstance is created withexplicit_bool=True, then booleans can be specified on the command line as--arg Trueor--arg Falserather than--arg. Additionally, booleans can be specified by prefixes ofTrueandFalsewith any capitalization as well as1or0(e.g. for True,--arg tRu,--arg T,--arg 1all suffice).OptionalThese arguments are parsed in exactly the same way asstr,int,float, andbool. Note bools can be specified using the same rules as above and thatOptionalis equivalent toOptional[str].ListIf an argumentargis aList, simply specify the values separated by spaces just as you would with regular argparse. For example,--arg 1 2 3parses toarg = [1, 2, 3].SetIdentical toListbut parsed into a set rather than a list.TupleTuples can be used to specify a fixed number of arguments with specified types using the syntaxTuple[Type1, Type2, etc.](e.g.Tuple[str, int, bool, str]). Tuples with a variable number of arguments are specified byTuple[Type, ...](e.g.Tuple[int, ...]). NoteTupledefaults toTuple[str, ...].LiteralLiteral is analagous to argparse'schoices, which specifies the values that an argument can take. For example, if arg can only be one of 'H', 1, False, or 1.0078 then you would specify thatarg: Literal['H', 1, False, 1.0078]. For instance,--arg Falseassigns arg to False and--arg Truethrows error.UnionUnion types must include thetypekeyword argument inadd_argumentin order to specify which type to use, as in the example below.defto_number(string:str)->Union[float,int]:returnfloat(string)if'.'instringelseint(string)classMyTap(Tap):number:Union[float,int]defconfigure(self):self.add_argument('--number',type=to_number)In Python 3.10+,Union[Type1, Type2, etc.]can be replaced withType1 | Type2 | etc., but thetypekeyword argument must still be provided inadd_argument.Complex TypesTap can also support more complex types than the ones specified above. If the desired type is constructed with a single string as input, then the type can be specified directly without additional modifications. For example,classPerson:def__init__(self,name:str)->None:self.name=nameclassArgs(Tap):person:Personargs=Args().parse_args('--person Tapper'.split())print(args.person.name)# TapperIf the desired type has a more complex constructor, then thetypekeyword argument must be provided inadd_argument. For example,classAgedPerson:def__init__(self,name:str,age:int)->None:self.name=nameself.age=agedefto_aged_person(string:str)->AgedPerson:name,age=string.split(',')returnAgedPerson(name=name,age=int(age))classArgs(Tap):aged_person:AgedPersondefconfigure(self)->None:self.add_argument('--aged_person',type=to_aged_person)args=Args().parse_args('--aged_person Tapper,27'.split())print(f'{args.aged_person.name}is{args.aged_person.age}')# Tapper is 27Argument processingWith complex argument parsing, arguments often end up having interdependencies. This means that it may be necessary to disallow certain combinations of arguments or to modify some arguments based on other arguments.To handle such cases, simply overrideprocess_argsand add the required logic.process_argsis automatically called whenparse_argsis called.classMyTap(Tap):package:stris_cool:boolstars:intdefprocess_args(self):# Validate argumentsifself.is_coolandself.stars<4:raiseValueError('Cool packages cannot have fewer than 4 stars')# Modify argumentsifself.package=='Tap':self.is_cool=Trueself.stars=5Processing known argsSimilar to argparse'sparse_known_args, Tap is capable of parsing only arguments that it is aware of without raising an error due to additional arguments. This can be done by callingparse_argswithknown_only=True. The remaining un-parsed arguments are then available by accessing theextra_argsfield of the Tap object.classMyTap(Tap):package:strargs=MyTap().parse_args(['--package','Tap','--other_arg','value'],known_only=True)print(args.extra_args)# ['--other_arg', 'value']SubclassingIt is sometimes useful to define a template Tap and then subclass it for different use cases. Since Tap is a native Python class, inheritance is built-in, making it easy to customize from a template Tap.In the example below,StarsTapandAwardsTapinherit the arguments (packageandis_cool) and the methods (process_args) fromBaseTap.classBaseTap(Tap):package:stris_cool:booldefprocess_args(self):ifself.package=='Tap':self.is_cool=TrueclassStarsTap(BaseTap):stars:intclassAwardsTap(BaseTap):awards:List[str]PrintingTap uses Python'spretty printerto print out arguments in an easy-to-read format."""main.py"""fromtapimportTapfromtypingimportListclassMyTap(Tap):package:stris_cool:bool=Trueawards:List[str]=['amazing','wow','incredible','awesome']args=MyTap().parse_args()print(args)Runningpython main.py --package Tapresults in:>>> python main.py
{'awards': ['amazing', 'wow', 'incredible', 'awesome'],
 'is_cool': True,
 'package': 'Tap'}ReproducibilityTap makes reproducibility easy, especially when running code in a git repo.Reproducibility infoSpecifically, Tap has a method calledget_reproducibility_infothat returns a dictionary containing all the information necessary to replicate the settings under which the code was run. This dictionary includes:Python commandThe Python command that was used to run the programEx.python main.py --package TapTimeThe time when the command was runEx.Thu Aug 15 00:09:13 2019Git rootThe root of the git repo containing the code that was runEx./Users/swansonk14/typed-argument-parserGit urlThe url to the git repo, specifically pointing to the current git hash (i.e. the hash of HEAD in the local repo)Ex.https://github.com/swansonk14/typed-argument-parser/tree/446cf046631d6bdf7cab6daec93bf7a02ac00998Uncommitted changesWhether there are any uncommitted changes in the git repo (i.e. whether the code is different from the code at the above git hash)Ex.TrueorFalseSaving and loading argumentsSaveTap has a method calledsavewhich saves all arguments, along with the reproducibility info, to a JSON file."""main.py"""fromtapimportTapclassMyTap(Tap):package:stris_cool:bool=Truestars:int=5args=MyTap().parse_args()args.save('args.json')After runningpython main.py --package Tap, the fileargs.jsonwill contain:{
    "is_cool": true,
    "package": "Tap",
    "reproducibility": {
        "command_line": "python main.py --package Tap",
        "git_has_uncommitted_changes": false,
        "git_root": "/Users/swansonk14/typed-argument-parser",
        "git_url": "https://github.com/swansonk14/typed-argument-parser/tree/446cf046631d6bdf7cab6daec93bf7a02ac00998",
        "time": "Thu Aug 15 00:18:31 2019"
    },
    "stars": 5
}Note: More complex types will be encoded in JSON as a pickle string.Load:exclamation: :warning:Never callargs.load('args.json')on untrusted files. Argument loading uses thepicklemodule to decode complex types automatically. Unpickling of untrusted data is a security risk and can lead to arbitrary code execution. Seethe warning in the pickle docs.:exclamation: :warning:Arguments can be loaded from a JSON file rather than parsed from the command line."""main.py"""fromtapimportTapclassMyTap(Tap):package:stris_cool:bool=Truestars:int=5args=MyTap()args.load('args.json')Note: All required arguments (in this casepackage) must be present in the JSON file if not already set in the Tap object.Load from dictArguments can be loaded from a Python dictionary rather than parsed from the command line."""main.py"""fromtapimportTapclassMyTap(Tap):package:stris_cool:bool=Truestars:int=5args=MyTap()args.from_dict({'package':'Tap','stars':20})Note: As withload, all required arguments must be present in the dictionary if not already set in the Tap object. All values in the provided dictionary will overwrite values currently in the Tap object.Loading from configuration filesConfiguration files can be loaded along with arguments with the optional flagconfig_files: List[str]. Arguments passed in from the command line overwrite arguments from the configuration files. Arguments in configuration files that appear later in the list overwrite the arguments in previous configuration files.For example, if you have the config filemy_config.txt--arg1 1
--arg2 twothen you can writefromtapimportTapclassArgs(Tap):arg1:intarg2:strargs=Args(config_files=['my_config.txt']).parse_args()Config files are parsed usingshlex.splitfrom the python standard library, which supports shell-style string quoting, as well as line-end comments starting with#.For example, if you have the config filemy_config_shlex.txt--arg1 21 # Important arg value

# Multi-word quoted string
--arg2 "two three four"then you can writefromtapimportTapclassArgs(Tap):arg1:intarg2:strargs=Args(config_files=['my_config_shlex.txt']).parse_args()to get the resultingargs = {'arg1': 21, 'arg2': 'two three four'}The legacy parsing behavior of using standard string split can be re-enabled by passinglegacy_config_parsing=Truetoparse_args.tapifytapifymakes it possible to run functions or initialize objects via command line arguments. This is inspired by Google'sPython Fire, buttapifyalso automatically casts command line arguments to the appropriate types based on the type hints. Under the hood,tapifyimplicitly creates a Tap object and uses it to parse the command line arguments, which it then uses to run the function or initialize the class. We show a few examples below.ExamplesFunction# square_function.pyfromtapimporttapifydefsquare(num:float)->float:"""Square a number.:param num: The number to square."""returnnum**2if__name__=='__main__':squared=tapify(square)print(f'The square of your number is{squared}.')Runningpython square_function.py --num 5printsThe square of your number is 25.0..Class# square_class.pyfromtapimporttapifyclassSquarer:def__init__(self,num:float)->None:"""Initialize the Squarer with a number to square.:param  num: The number to square."""self.num=numdefget_square(self)->float:"""Get the square of the number."""returnself.num**2if__name__=='__main__':squarer=tapify(Squarer)print(f'The square of your number is{squarer.get_square()}.')Runningpython square_class.py --num 2printsThe square of your number is 4.0..Dataclass# square_dataclass.pyfromdataclassesimportdataclassfromtapimporttapify@dataclassclassSquarer:"""Squarer with a number to square.:param  num: The number to square."""num:floatdefget_square(self)->float:"""Get the square of the number."""returnself.num**2if__name__=='__main__':squarer=tapify(Squarer)print(f'The square of your number is{squarer.get_square()}.')Runningpython square_dataclass.py --num -1printsThe square of your number is 1.0..tapify helpThe help string on the command line is set based on the docstring for the function or class. For example, runningpython square_function.py -hwill print:usage: square_function.py [-h] --num NUM

Square a number.

options:
  -h, --help  show this help message and exit
  --num NUM   (float, required) The number to square.Note that for classes, if there is a docstring in the__init__method, thentapifysets the help string description to that docstring. Otherwise, it uses the docstring from the top of the class.Command line vs explicit argumentstapifycan simultaneously use both arguments passed from the command line and arguments passed in explicitly in thetapifycall. Arguments provided in thetapifycall override function defaults, and arguments provided via the command line override both arguments provided in thetapifycall and function defaults. We show an example below.# add.pyfromtapimporttapifydefadd(num_1:float,num_2:float=0.0,num_3:float=0.0)->float:"""Add numbers.:param num_1: The first number.:param num_2: The second number.:param num_3: The third number."""returnnum_1+num_2+num_3if__name__=='__main__':added=tapify(add,num_2=2.2,num_3=4.1)print(f'The sum of your numbers is{added}.')Runningpython add.py --num_1 1.0 --num_2 0.9printsThe sum of your numbers is 6.0.. (Note thataddtooknum_1 = 1.0andnum_2 = 0.9from the command line andnum_3=4.1from thetapifycall due to the order of precedence.)Known argsCallingtapifywithknown_only=Trueallowstapifyto ignore additional arguments from the command line that are not needed for the function or class. Ifknown_only=False(the default), thentapifywill raise an error when additional arguments are provided. We show an example below whereknown_only=Truemight be useful for running multipletapifycalls.# person.pyfromtapimporttapifydefprint_name(name:str)->None:"""Print a person's name.:param name: A person's name."""print(f'My name is{name}.')defprint_age(age:int)->None:"""Print a person's age.:param name: A person's age."""print(f'My age is{age}.')if__name__=='__main__':tapify(print_name,known_only=True)tapify(print_age,known_only=True)Runningpython person.py --name Jesse --age 1printsMy name is Jesse.followed byMy age is 1.. Withoutknown_only=True, thetapifycalls would raise an error due to the extra argument.