Command-Line Interfaces (argparse)
15 questions found
How would you build a CLI tool where some arguments can also be set via environment variables, falling back to a default if neither is provided?
Advanced
Read the relevant environment variable with os.environ.get() as the DEFAULT value passed to add_argument(), so an explicit command-line flag always overrides the environment variable, which in turn overrides a final hardcoded fallback default.
import os
parser.add_argument(
"--api-key",
default=os.environ.get("MY_APP_API_KEY"),
required=os.environ.get("MY_APP_API_KEY") is None,
help="API key (or set MY_APP_API_KEY environment variable)"
)
Real-world example
Allowing a CLI tool's API key or config path to be set via an environment variable in CI, while still supporting an explicit --api-key override locally.
Common follow-ups: What precedence order makes the most sense: CLI flag, environment variable, or config file default?
Modules & Packaging
How do you customize argparse's automatically-generated usage and help text formatting, like preserving specific line breaks in the description?
Advanced
Pass a custom 'formatter_class' (like argparse.RawDescriptionHelpFormatter or RawTextHelpFormatter) to ArgumentParser() to disable argparse's default text-wrapping/reformatting behavior, letting you control exact formatting of multi-line descriptions or examples in the help output.
parser = argparse.ArgumentParser(
description="""Deploy an application.
Examples:
mytool deploy --env prod
mytool deploy --env staging --dry-run""",
formatter_class=argparse.RawDescriptionHelpFormatter
)
Real-world example
Providing multi-line usage EXAMPLES in a CLI tool's --help output that preserve their intended formatting exactly.
Common follow-ups: What's the difference between RawDescriptionHelpFormatter and RawTextHelpFormatter?
Modules & Packaging
How would you write integration tests for an argparse-based CLI tool using pytest, verifying both successful parsing and expected error behavior?
Advanced
Call your parser's parse_args() directly with an explicit list of strings (bypassing sys.argv entirely) to test parsing logic in isolation; to test error behavior (which normally calls sys.exit()), wrap the call in pytest.raises(SystemExit) and optionally capture stderr with the 'capsys' fixture to assert on the error message.
import pytest
def test_valid_args():
args = parser.parse_args(["--count", "5"])
assert args.count == 5
def test_invalid_choice(capsys):
with pytest.raises(SystemExit):
parser.parse_args(["--log-level", "invalid"])
captured = capsys.readouterr()
assert "invalid choice" in captured.err
Real-world example
Writing a reliable pytest suite verifying a CLI tool's argument parsing behaves correctly for both valid and invalid inputs.
Common follow-ups: Why does argparse call sys.exit() on a parsing error instead of raising a normal, catchable exception?
Testing (pytest)
How do you build a plugin-style CLI where subcommands are dynamically discovered and registered from separate modules, rather than hardcoded?
Advanced
Use Python's entry_points mechanism (declared in pyproject.toml) or a simple plugin-discovery pattern (scanning a 'commands' package/directory) to dynamically import each subcommand module and call a shared 'register(subparsers)' function it exposes — letting new subcommands be added by simply dropping in a new file, without modifying the main CLI script.
# commands/deploy.py
def register(subparsers):
parser = subparsers.add_parser("deploy")
parser.add_argument("--env", required=True)
parser.set_defaults(func=lambda args: deploy(args.env))
# main.py
for module in discover_command_modules():
module.register(subparsers)
args = parser.parse_args()
args.func(args) # dispatches to the registered handler
Real-world example
Building an extensible CLI tool (like a plugin-based deployment tool) where new subcommands can be added without touching the core script.
Common follow-ups: How does 'set_defaults(func=...)' provide a clean way to dispatch parsed arguments to the correct handler function?
Modules & Packaging
How do you provide a short and a long form for the same flag, like -v and --verbose?
Intermediate
Pass both flag strings as positional arguments to add_argument() in the order you want them displayed; argparse treats them as aliases for the same destination attribute, and by default uses the first long-form name (stripped of leading dashes) as the attribute name unless you override it with 'dest'.
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output")
args = parser.parse_args(["-v"])
print(args.verbose) # True -- both -v and --verbose set the same 'verbose' attribute
Real-world example
Supporting both a quick short flag (-v) for interactive use and a clearer long flag (--verbose) for scripts, referencing the same option.
Common follow-ups: Can a single argument have more than two aliases, like -v, --verbose, and --debug all pointing to the same flag?
Data Types & Structures