Package: datargs
datargsA paper-thin wrapper aroundargparsethat creates type-safe parsers
fromdataclassandattrsclasses.QuickstartInstalldatargs:pipinstalldatargsCreate adataclass(or anattrsclass) describing your command line interface, and calldatargs.parse()with the class:# script.pyfromdataclassesimportdataclassfrompathlibimportPathfromdatargsimportparse@dataclass# or @attr.s(auto_attribs=True)classArgs:url:stroutput_path:Pathverbose:boolretries:int=3defmain():args=parse(Args)print(args)if__name__=="__main__":main()(experimental)Alternatively: convert an existing parser to a dataclass:# script.pyparser=ArgumentParser()parser.add_argument(...)fromdatargsimportconvertconvert(parser)convert()prints a class definition to the console.
Copy it to your script.Mypy and pycharm correctly infer the type ofargsasArgs, and your script is good to go!$pythonscript.py-h
usage:test.py[-h]--urlURL--output-pathOUTPUT_PATH[--retriesRETRIES][--verbose]optionalarguments:-h,--helpshowthishelpmessageandexit--urlURL--output-pathOUTPUT_PATH--retriesRETRIES--verbose
$pythonscript.py--url"https://..."--output-pathout--retries4--verbose
Args(url="https://...",output_path=Path("out"),retries=4,verbose=True)Table of ContentsFeaturesStatic verificationdataclass/attr.sagnosticAliasesArgumentParseroptionsEnumsSequences, Optionals, and LiteralsSub Commands"Why not"s and design choicesJust use argparse?Useclick?Useclout?Usesimple-parsing?Useargparse-dataclass?Useargparse-dataclasses?FAQsIs this cross-platform?Why are mutually exclusive options not supported?FeaturesStatic verificationMypy/Pycharm have your back when you when you make a mistake:...defmain():args=parse(Args)args.urll# typo...Pycharm says:Unresolved attribute reference 'urll' for class 'Args'.Mypy says:script.py:15: error: "Args" has no attribute "urll"; maybe "url"?dataclass/attr.sagnostic>>>importattr,datargs>>>@attr.s...classArgs:...flag:bool=attr.ib()>>>datargs.parse(Args,[])Args(flag=False)AliasesAliases andArgumentParser.add_argument()parameters are taken frommetadata:>>>fromdataclassesimportdataclass,field>>>fromdatargsimportparse>>>@dataclass...classArgs:...retries:int=field(default=3,metadata=dict(help="number of retries",aliases=["-r"],metavar="RETRIES"))>>>parse(Args,["-h"])usage: ...optional arguments:-h, --help            show this help message and exit--retries RETRIES, -r RETRIES>>>parse(Args,["-r","4"])Args(retries=4)argis a replacement forfieldthat putsadd_argument()parameters inmetadataand makesaliasesbehaves like in the original method. Use it to save precious keystrokes:>>>fromdataclassesimportdataclass>>>fromdatargsimportparse,arg>>>@dataclass...classArgs:...retries:int=arg("-r",default=3,help="number of retries",metavar="RETRIES")>>>parse(Args,["-h"])# exactly the same as beforeNOTE:arg()does not currently work withattr.s.arg()also supports allfield/attr.ib()keyword arguments.ArgumentParseroptionsYou can passArgumnetParserkeyword arguments toargsclass.
Description is its own parameter - the rest are passed as theparser_paramsparameter as adict.When a class is used as a subcommand (see below),parser_paramsare passed toadd_parser, includingaliases.>>>fromdatargsimportparse,argsclass>>>@argsclass(description="Romans go home!",parser_params=dict(prog="messiah.py"))...classArgs:...flag:bool>>>parse(Args,["-h"],parser=parser)usage: messiah.py [-h] [--flag]Romans go home!...or you can pass your own parser:>>>fromargparseimportArgumentParser>>>fromdatargsimportparse,argsclass>>>@argsclass...classArgs:...flag:bool>>>parser=ArgumentParser(description="Romans go home!",prog="messiah.py")>>>parse(Args,["-h"],parser=parser)usage: messiah.py [-h] [--flag]Romans go home!...Usemake_parser()to create a parser and save it for later:>>>fromdatargsimportmake_parser>>>@dataclass...classArgs:......>>>parser=make_parser(Args)# pass `parser=...` to modify an existing parserNOTE: passing your own parser ignoresArgumentParserparams passed toargsclass().EnumsWithdatargs, enums Just Work™:>>>importenum,attr,datargs>>>classFoodEnum(enum.Enum):...ham=0...spam=1>>>@attr.dataclass...classArgs:...food:FoodEnum>>>datargs.parse(Args,["--food","ham"])Args(food=<FoodEnum.ham: 0>)>>>datargs.parse(Args,["--food","eggs"])usage: enum_test.py [-h] --food {ham,spam}enum_test.py: error: argument --food: invalid choice: 'eggs' (choose from ['ham', 'spam'])NOTE: enums are passed by name on the command line and not by value.Sequences, Optionals, and LiteralsHave aSequenceor aListof something to
automatically usenargs:frompathlibimportPathfromdataclassesimportdataclassfromtypingimportSequencefromdatargsimportparse@dataclassclassArgs:# same as nargs='*'files:Sequence[Path]=()args=parse(Args,["--files","foo.txt","bar.txt"])assertargs.files==[Path("foo.txt"),Path("bar.txt")]Specify a list of positional parameters like so:fromdatargsimportargsclass,arg@argsclassclassArgs:arg:Sequence[int]=arg(default=(),positional=True)Optionalarguments default toNone:frompathlibimportPathfromdataclassesimportdataclassfromtypingimportOptionalfromdatargsimportparse@dataclassclassArgs:path:Optional[Path]=Noneargs=parse(Args,["--path","foo.txt"])assertargs.path==Path("foo.txt")args=parse(Args,[])assertargs.pathisNoneAndLiteralcan be used to specify choices:frompathlibimportPathfromdataclassesimportdataclassfromtypingimportLiteralfromdatargsimportparse@dataclassclassArgs:path:Literal[Path("foo.txt"),Path("bar.txt")]args=parse(Args,["--path","foo.txt"])assertargs.path==Path("foo.txt")# Throws an error!args=parse(Args,["--path","bad-option.txt"])Sub CommandsNo need to specify a uselessdestto dispatch on different commands.
AUnionof dataclasses/attrs classes automatically becomes a group of subparsers.
The attribute holding theUnionholds the appropriate instance
upon parsing, making your code type-safe:importtyping,loggingfromdatargsimportargsclass,arg,parse@argsclass(description="install package")classInstall:package:str=arg(positional=True,help="package to install")@argsclass(description="show all packages")classShow:verbose:bool=arg(help="show extra info")@argsclass(description="Pip Install Packages!")classPip:action:typing.Union[Install,Show]log:str=Noneargs=parse(Pip,["--log","debug.log","install","my_package"])print(args)# prints: Pip(action=Install(package='my_package'), log='debug.log')# Consume arguments:ifargs.log:logging.basicConfig(filename=args.log)ifisinstance(args.action,Install):install_package(args.action.package)# static type error: args.action.verboseelifisinstance(args.action,Show):list_all_packages(verbose=args.action.verbose)else:assertFalse,"Unreachable code"Command name is derived from class name. To change this, use thenameparameter to@argsclass.As with all other parameters toadd_parser,aliasescan be passed as a key inparser_paramsto add subcommand aliases.NOTE: if the commented-out line above does not issue a type error, try adding an@dataclass/@attr.sbefore or instead of@argsclass():@argsclass(description="Pip Install Packages!")# optional@dataclassclassPip:action:typing.Union[Install,Show]log:str=None...ifisinstance(args.action,Install):install_package(args.action.package)# this should now produce a type error: args.action.verbose"Why not"s and design choicesMany libraries out there do similar things. This list serves as documentation for existing solutions and differences.So, why not...Just use argparse?That's easy. The interface is clumsy and repetitive, a.k.a boilerplate. Additionally,ArgumentParser.parse_args()returns aNamespace, which is
equivalent toAny, meaning that it any attribute access is legal when type checking. Alas, invalid attribute access will fail at runtime. For example:defparse_args():parser=ArgumentParser()parser.add_argument("--url")returnparser.parse_args()defmain():args=parse_args()print(args.url)Let's say for some reason--urlis changed to--uri:parser.add_argument("--uri")...print(args.url)# oopsYou won't discover you made a mistake until you run the code. Withdatargs, a static type checker will issue an error.
Also, why use a carriage when you have a spaceship?Useclick?clickis a great library. It provides many utilities for command line programs.Usedatargsif you believe user interface should not be coupled with implementation, or if you
want to useargparsewithout boilerplate.
Useclickif you don't care.Useclout?It seems thatcloutaims to be an end-to-end solution for command line programs à la click.Use it if you need a broader solution. Usedatargsif you want to useargparsewithout boilerplate.Usesimple-parsing?This is another impressive library.Use it if you have deeply-nested options, or if the following points don't apply
to you.Usedatargsif you:needattrssupportwant as little magic as possibledon't have many options or they're not nestedprefer dashes (--like-this) over underscores (--like_this)Useargparse-dataclass?It's similar to this library. The main differences I found are:noattrssupportnot on github, so who you gonna call?Useargparse-dataclasses?Same pointsargparse-dataclassbut alsoUses inheritance.FAQsIs this cross-platform?Yes, just likeargparse.
If you find a bug on a certain platform (or any other bug), please report it.Why are mutually exclusive options not supported?This library is based on the idea of a one-to-one correspondence between most parsers
and simple classes. Conceptually, mutually exclusive options are analogous tosum types, just likesubparsersare,
but writing a class for each flag is not ergonomic enough.
Contact me if you want this feature or if you come up with a better solution.