|$ curl https://forge-ai.dev/api/markdown?path=docs/python/argparse
$cat docs/argparse.md
updated Today·18-24 min read·published

argparse

PythonCLIargparseIntermediate🎯Free Tools
Introduction

argparse is the stdlib toolkit for command-line interfaces. It validates types, generates --help, and supports subcommands. For ergonomic typed CLIs, many teams also use Typer (built on Click) — covered briefly below.

Design CLIs like APIs: clear verbs, validated inputs, non-zero exit codes on failure, and help text that teaches usage.

info

Prefer type=Path and choices over post-hoc string checks. Pair with pathlib.
Basic Parser
basic.py
Python
1import argparse
2from pathlib import Path
3
4def build_parser() -> argparse.ArgumentParser:
5 p = argparse.ArgumentParser(
6 prog="forge",
7 description="Example ForgeLearn CLI",
8 epilog="Docs: https://forgelearn.dev/docs/python/argparse",
9 )
10 p.add_argument("input", type=Path, help="input file path")
11 p.add_argument("-o", "--output", type=Path, default=Path("out.txt"))
12 p.add_argument("-n", "--count", type=int, default=1, help="repeat count")
13 p.add_argument("-v", "--verbose", action="count", default=0,
14 help="-v / -vv for more logging")
15 p.add_argument("--format", choices=["json", "csv", "text"], default="text")
16 return p
17
18if __name__ == "__main__":
19 args = build_parser().parse_args()
20 print(args.input, args.output, args.count, args.verbose, args.format)
add_argument patternMeaning
"input", type=PathPositional required Path
--flag, action=store_trueBoolean flag
action=countIncrement per -v
choices=[...]Enum-like restriction
nargs='+'One or more values
nargs='*'Zero or more
required=True on optionalForce --opt
Custom Types & Validation
types.py
Python
1import argparse
2from pathlib import Path
3
4def existing_file(s: str) -> Path:
5 p = Path(s)
6 if not p.is_file():
7 raise argparse.ArgumentTypeError(f"not a file: {s}")
8 return p
9
10def positive_int(s: str) -> int:
11 n = int(s)
12 if n <= 0:
13 raise argparse.ArgumentTypeError("must be > 0")
14 return n
15
16parser = argparse.ArgumentParser()
17parser.add_argument("config", type=existing_file)
18parser.add_argument("--workers", type=positive_int, default=4)
19args = parser.parse_args()

best practice

Raise ArgumentTypeError from custom type callables — argparse turns them into clean usage errors.
Subcommands

Use add_subparsers for git-like verbs: tool run, tool list.

subcommands.py
Python
1import argparse
2from pathlib import Path
3
4def cmd_run(args: argparse.Namespace) -> int:
5 print("run", args.job, "dry" if args.dry_run else "live")
6 return 0
7
8def cmd_list(args: argparse.Namespace) -> int:
9 for p in Path(args.dir).glob(args.pattern):
10 print(p)
11 return 0
12
13def main(argv: list[str] | None = None) -> int:
14 parser = argparse.ArgumentParser(prog="jobs")
15 sub = parser.add_subparsers(dest="command", required=True)
16
17 run_p = sub.add_parser("run", help="run a job")
18 run_p.add_argument("job")
19 run_p.add_argument("--dry-run", action="store_true")
20 run_p.set_defaults(func=cmd_run)
21
22 list_p = sub.add_parser("list", help="list files")
23 list_p.add_argument("--dir", type=Path, default=Path("."))
24 list_p.add_argument("--pattern", default="*")
25 list_p.set_defaults(func=cmd_list)
26
27 args = parser.parse_args(argv)
28 return args.func(args)
29
30if __name__ == "__main__":
31 raise SystemExit(main())
Mutually Exclusive Groups
mutex.py
Python
1import argparse
2
3parser = argparse.ArgumentParser()
4g = parser.add_mutually_exclusive_group(required=True)
5g.add_argument("--json", action="store_true", help="JSON output")
6g.add_argument("--plain", action="store_true", help="plain text")
7g.add_argument("--csv", action="store_true", help="CSV output")
8
9auth = parser.add_argument_group("authentication")
10auth.add_argument("--token")
11auth.add_argument("--user")
12
13args = parser.parse_args()

Also useful: add_argument_group for help-section organization without mutual exclusion.

Defaults from Environment
env_defaults.py
Python
1import argparse
2import os
3
4parser = argparse.ArgumentParser()
5parser.add_argument(
6 "--host",
7 default=os.getenv("APP_HOST", "127.0.0.1"),
8 help="bind host (env APP_HOST)",
9)
10parser.add_argument(
11 "--port",
12 type=int,
13 default=int(os.getenv("APP_PORT", "8000")),
14)
15args = parser.parse_args()
🔥

pro tip

For complex config, parse CLI with argparse then validate with Pydantic Settings.
Typer Overview

Typer gives you type-hint-driven CLIs with automatic help. Great for internal tools; argparse remains ideal when you want zero dependencies.

typer_demo.py
Python
1# pip install typer
2import typer
3from pathlib import Path
4
5app = typer.Typer(help="Typer demo")
6
7@app.command()
8def convert(
9 input: Path = typer.Argument(..., exists=True, readable=True),
10 output: Path = typer.Option(Path("out.json")),
11 verbose: bool = False,
12) -> None:
13 """Convert INPUT to OUTPUT."""
14 if verbose:
15 typer.echo(f"{input} -> {output}")
16 output.write_text(input.read_text(encoding="utf-8").upper(), encoding="utf-8")
17
18@app.command("list-ext")
19def list_ext(ext: str = ".py") -> None:
20 for p in Path(".").rglob(f"*{ext}"):
21 typer.echo(p)
22
23if __name__ == "__main__":
24 app()
Exit Codes & Testing
testing_cli.py
Python
1import argparse
2
3def parse(argv: list[str]) -> argparse.Namespace:
4 p = argparse.ArgumentParser()
5 p.add_argument("--n", type=int, required=True)
6 return p.parse_args(argv)
7
8def main(argv: list[str] | None = None) -> int:
9 try:
10 args = parse(argv if argv is not None else None)
11 except SystemExit as e:
12 return int(e.code or 0)
13 if args.n < 0:
14 print("n must be >= 0", file=__import__("sys").stderr)
15 return 2
16 print(args.n * 2)
17 return 0
18
19# pytest: assert main(["--n", "3"]) == 0
CodeConvention
0Success
1Runtime / unexpected failure
2CLI usage error (argparse default)
130SIGINT (128+2)
Production Patterns
  • Keep build_parser() separate from main() for testability
  • Use set_defaults(func=...) for subcommand dispatch
  • Document env overrides in help strings
  • Never trust raw strings for paths — type=Path + existence checks
  • Return int from main(); raise SystemExit(main())
production.py
Python
1"""Production-shaped CLI entrypoint."""
2from __future__ import annotations
3import argparse
4import logging
5from pathlib import Path
6
7log = logging.getLogger("forge")
8
9def build_parser() -> argparse.ArgumentParser:
10 p = argparse.ArgumentParser(prog="forge")
11 p.add_argument("path", type=Path)
12 p.add_argument("-v", "--verbose", action="store_true")
13 return p
14
15def run(path: Path) -> None:
16 text = path.read_text(encoding="utf-8")
17 log.info("read %s bytes from %s", len(text), path)
18
19def main(argv: list[str] | None = None) -> int:
20 args = build_parser().parse_args(argv)
21 logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
22 try:
23 run(args.path)
24 except OSError as e:
25 log.error("%s", e)
26 return 1
27 return 0
28
29if __name__ == "__main__":
30 raise SystemExit(main())
nargs, const & append

Advanced argument shapes: optional value with const, accumulating lists, and remainder args.

nargs.py
Python
1import argparse
2
3p = argparse.ArgumentParser()
4p.add_argument("--mode", nargs="?", const="auto", default="off",
5 help="omit->off, --mode->auto, --mode X->X")
6p.add_argument("--tag", action="append", default=[], help="repeatable")
7p.add_argument("--ids", nargs="+", type=int, help="one or more ints")
8p.add_argument("paths", nargs="*", help="zero or more paths")
9p.add_argument("rest", nargs=argparse.REMAINDER, help="after --")
10# example: tool --tag a --tag b --ids 1 2 3 -- --weird
11print(p.parse_args())
Parent Parsers & Sharing Flags
parents.py
Python
1import argparse
2
3common = argparse.ArgumentParser(add_help=False)
4common.add_argument("-v", "--verbose", action="store_true")
5common.add_argument("--config", default="config.toml")
6
7main = argparse.ArgumentParser(parents=[common])
8sub = main.add_subparsers(dest="cmd", required=True)
9sub.add_parser("build", parents=[common], help="build artifact")
10sub.add_parser("deploy", parents=[common], help="deploy artifact")
11print(main.parse_args(["build", "-v"]))
Help Formatting
help_fmt.py
Python
1import argparse
2
3class Raw(argparse.ArgumentDefaultsHelpFormatter,
4 argparse.RawDescriptionHelpFormatter):
5 pass
6
7p = argparse.ArgumentParser(
8 formatter_class=Raw,
9 description="Line 1\nLine 2 preserved",
10)
11p.add_argument("--workers", type=int, default=4, help="pool size")
12p.print_help()
$Blueprint — Engineering Documentation·Section ID: PYTHON-ARGPARSE·Revision: 1.0

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.