MLArgParser is a multi-level argument parser for building CLI applications in Python. It maps object-oriented concepts directly to the command line: subclasses become subcommands, methods become commands, and method parameters become command-line arguments. Type hints and docstrings drive help text and type conversion automatically.
Requirements: Python 3.8+ (3.9+ recommended for exit_on_error=False and built-in generics like list[str]).
- Commands are the public methods of your parser class (names not starting with
_). - Arguments are the parameters of those methods; names and types come from the signature.
- Help text comes from the class/method docstrings and from the
arg_descdictionary. - Subcommands are implemented by assigning another
MLArgParsersubclass as a class attribute, forming a tree of commands.
The library uses argparse under the hood. You get standard help formatting, long and short options, and consistent error handling without writing parser setup code by hand.
Subclass MLArgParser and define methods; their names become commands. Use type hints and defaults for arguments; use arg_desc to describe them in help.
#!/usr/bin/env python3
from mlargparser import MLArgParser
class MyApp(MLArgParser):
"""My application."""
arg_desc = {
"count": "Number of items",
"name": "Item name",
"format": "Output format",
}
def show(self, count: int = 10, name: str = None):
"""Show items."""
print(f"count={count}, name={name}")
def run(self, format: str = "text"):
"""Run the task."""
print(f"format={format}")
if __name__ == "__main__":
MyApp()Example invocations:
./myapp.py --help
./myapp.py show --count 5 --name foo
./myapp.py run --format json
Public method names are normalized for the CLI: underscores become dashes, and by default command names are lowercased.
Every public method (no leading _) is a command. The command name is derived from the method name: underscores are replaced with dashes, and by default the result is lowercased (e.g. dump_config becomes dump-config).
Parameter type hints determine how values are parsed and passed to your method:
| Annotation | CLI behavior |
|---|---|
str |
One string (default if no annotation) |
int |
One integer |
float |
One float |
bool |
Flag; see Boolean flags below |
list[T] |
One or more values, collected as list |
set[T] |
One or more values, collected as set |
tuple[T, ...] |
One or more values, as tuple |
Optional[T] / Union[T, None] |
Unwraps to T |
Unannotated parameters and None are treated as str. Invalid or unresolved annotations are reported at startup when strict_types=True (default).
- No default (or
inspect.Parameter.empty) means the argument is required. - A default value makes the argument optional; the default is shown in help.
Set arg_desc on your class (or subclass) to map parameter names to help strings:
arg_desc = {
"count": "Number of items to process",
"output": "Output file path",
}If a parameter is not in arg_desc, help uses the placeholder FIXME: UNDOCUMENTED. Subparsers merge their parent’s arg_desc with their own; local entries override the parent’s for the same key.
Boolean parameters are turned into flags:
- Default
False: one flag that turns the value toTrue(e.g.--verbose). - Default
True: one flag that turns it toTrue(redundant but explicit) and, by default, a--no-<name>flag that turns it toFalse(e.g.--no-cache). - Parameter name starts with
no_: treated as the “off” side of a flag; the option is--no-<rest>and sets the base name toFalse(e.g.no_cache->--no-cacheanddestcache).
You must not define both a foo and a no_foo parameter for the same logical flag; that is rejected as ambiguous. Set auto_disable_flags = False on your class to disable automatic --no-* generation for True-default booleans.
To add a subcommand level, assign an MLArgParser subclass as a class attribute. That class is then instantiated when the user selects that command; it parses the rest of argv and dispatches to its own commands.
Example: one top-level command dump with subcommands config, state, and authtoken:
class DumpCmd(MLArgParser):
"""Dump subcommand."""
def config(self):
"""Dump configuration."""
...
def state(self):
"""Dump state."""
...
def authtoken(self):
"""Dump auth token."""
...
class MyApp(MLArgParser):
"""Main application."""
dump = DumpCmdInvocation:
./app.py dump config
./app.py dump state
./app.py dump authtoken
When the user runs ./app.py dump config, the top-level parser sees the command dump, gets the class DumpCmd, and calls DumpCmd(level=2, parent=app, top=app). That sub-parser then parses config and invokes DumpCmd.config(). You can nest further by assigning another parser class as an attribute of DumpCmd, and so on.
Inside a subcommand, self.parent is the immediate parent parser instance and self.top is the root parser instance (e.g. MyApp), which is useful for sharing state or configuration.
For each argument the library adds a long option --<name> (with underscores in the name turned into dashes). If the first character of the argument name is not already used by another argument, a short option -<letter> is also added. So for a parameter verbose, you get both --verbose and -v unless -v was already taken.
Set these as class attributes on your parser class (or subclass):
| Attribute | Default | Description |
|---|---|---|
arg_desc |
None |
Dict mapping parameter names to help strings. |
auto_disable_flags |
True |
If True, add --no-<name> for boolean parameters with default True. |
case_sensitive_commands |
False |
If True, command names are not lowercased. |
strict_validation |
True |
If True, command name collisions and (when strict_types is also True) type validation errors are fatal. |
strict_types |
True |
If True, invalid or unresolved type annotations cause startup to fail; if False, they are reported as warnings. |
Constructor:
MLArgParser(level=1, parent=None, top=None, noparse=False, strict_types=True)
Normally you do not call this with customlevel/parent/top; they are used internally for subcommands. Usenoparse=Trueonly in tests or when you need to set up the parser without parsingsys.argv(e.g. to build help or run a specific command programmatically).
Optional bash/zsh tab completion is provided via argcomplete. Install the extra and enable it in your script:
pip install mlargparser[argcomplete]# PYTHON_ARGCOMPLETE_OK
from mlargparser import MLArgParser
import mlargparser_argcomplete
mlargparser_argcomplete.install()
class MyApp(MLArgParser):
...
if __name__ == "__main__":
MyApp()For global completion (any script with PYTHON_ARGCOMPLETE_OK is completed without per-command registration), run once:
activate-global-python-argcompleteTo register a single command instead:
eval "$(register-python-argcomplete myapp)"- The top-level description is the class docstring.
- Each command’s description is that method’s docstring.
- Each argument’s help comes from
arg_descor the undocumented placeholder. - Defaults are appended where applicable (e.g.
[default: "text"],[enabled by default]).
Tests live under tests/ and use the standard library unittest:
python3 -m unittest discover -s tests -p "test_*.py" -vUnless otherwise noted, code in this repository is licensed under the LGPL v2 only. For use under a different license, contact the author.
- argparse — Python standard library.
- PEP 484 — Type hints.
- Implementation inspired by Multi-level argparse.