Command-Line Arguments
Command-line arguments let a Python program accept input at launch — from raw sys.argv up through argparse's structured positional arguments, options, and validation.
Command-line arguments are the values a user supplies when starting a program from a shell: python report.py input.csv --verbose. Python exposes them raw via sys.argv, and provides the standard-library argparse module to parse them into structured, validated, self-documenting options.
sys.argv Basics
sys.argv is a list of strings: element 0 is the script's own name/path, and the rest are whatever arguments the user typed, in order, always as strings (no automatic type conversion).
import sys
print(sys.argv)
# Running: python report.py input.csv --verbose
# -> ['report.py', 'input.csv', '--verbose']
if len(sys.argv) > 1:
filename = sys.argv[1]
print(f"Processing {filename}")Limitations of Raw sys.argv
Working with sys.argv directly means manually handling: missing arguments (index errors), type conversion ("5" stays a string until you convert it), flag detection ("--verbose" in sys.argv), help text, and error messages — all by hand, and all differently in every script. This doesn't scale past the simplest one-argument script, which is exactly the gap argparse fills.
import sys
# Fragile: crashes with an unhelpful traceback if no argument is given
filename = sys.argv[1]argparse Overview
argparse is a standard-library module for declaring the arguments a program accepts, then parsing sys.argv (or an explicit list) against that declaration. It generates --help output automatically, converts types, enforces required/optional arguments, and reports usage errors in a consistent, user-friendly format.
import argparse
parser = argparse.ArgumentParser(description="Summarize a CSV report.")
parser.add_argument("filename", help="path to the CSV file")
parser.add_argument("--verbose", action="store_true", help="print extra detail")
args = parser.parse_args()
print(args.filename, args.verbose)Positional Arguments
A positional argument is required by default and identified by its position in the command line, not by a flag name. Declared with a plain name (no leading dashes).
Running the program with too few positional arguments produces a clear usage error and a non-zero exit code automatically — no manual index-checking needed.
parser = argparse.ArgumentParser()
parser.add_argument("source")
parser.add_argument("destination")
args = parser.parse_args()
# python cp.py a.txt b.txt -> args.source == "a.txt", args.destination == "b.txt"Options and Flags
An option takes a value and is identified by name (--output report.txt); a flag is a boolean switch that takes no value, just presence or absence (--verbose). Both are declared with a leading -- (or a short -x alias) and are optional by default.
action="store_true" makes an argument a flag whose presence sets the attribute to True (defaulting to False when absent); leaving action unset (the default, "store") makes it a value-taking option.
parser = argparse.ArgumentParser()
parser.add_argument("-o", "--output", help="output file path")
parser.add_argument("-v", "--verbose", action="store_true", help="verbose output")
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
# python run.py -o out.txt -v
# -> args.output == "out.txt", args.verbose == True, args.dry_run == FalseDefaults
default= supplies a value used when the argument is omitted entirely, so downstream code never has to check for None by hand for arguments that have a sensible fallback.
parser = argparse.ArgumentParser()
parser.add_argument("--retries", type=int, default=3)
parser.add_argument("--log-level", choices=["debug", "info", "warning"], default="info")
args = parser.parse_args([]) # simulate no arguments given
print(args.retries, args.log_level) # 3 infoType Conversion and Validation
type= converts the raw string argument (any callable that accepts a string and returns a value works, including int, float, or a custom function). choices= restricts allowed values and produces an automatic error for anything else. required=True makes an otherwise-optional (--flag-style) argument mandatory.
A type callable that raises ValueError on bad input (including int itself) produces a clean argparse error automatically — no manual try/except required in most cases.
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=8080)
parser.add_argument("--mode", choices=["fast", "safe"], required=True)
args = parser.parse_args(["--mode", "fast", "--port", "9090"])
print(args.port, args.mode) # 9090 fast (port is an int, not a string)
# An invalid choice or type raises a clear, self-generated error:
# parser.parse_args(["--mode", "turbo"])
# -> error: argument --mode: invalid choice: 'turbo' (choose from 'fast', 'safe')nargs and Repeated Arguments
nargs= controls how many values an argument consumes: "+" means one or more, "*" means zero or more, and action="append" lets a repeatable flag accumulate a list across multiple uses.
parser = argparse.ArgumentParser()
parser.add_argument("files", nargs="+")
parser.add_argument("--tag", action="append", default=[])
args = parser.parse_args(["a.txt", "b.txt", "--tag", "urgent", "--tag", "review"])
print(args.files) # ['a.txt', 'b.txt']
print(args.tag) # ['urgent', 'review']Subcommands
Tools with multiple distinct actions (like git commit or git push) use add_subparsers() so each subcommand can declare its own arguments independently.
parser = argparse.ArgumentParser(prog="taskcli")
subparsers = parser.add_subparsers(dest="command", required=True)
add_parser = subparsers.add_parser("add")
add_parser.add_argument("title")
list_parser = subparsers.add_parser("list")
list_parser.add_argument("--all", action="store_true")
args = parser.parse_args(["add", "Buy milk"])
print(args.command, args.title) # add "Buy milk"Practical Usage
A realistic small CLI script combining several of the above:
Running python linecount.py a.txt b.txt --verbose --min-lines 5 counts lines in each file, printing only files meeting the minimum, in verbose detail.
import argparse
from pathlib import Path
def main() -> None:
parser = argparse.ArgumentParser(description="Count lines in text files.")
parser.add_argument("files", nargs="+", type=Path, help="files to count")
parser.add_argument("-v", "--verbose", action="store_true")
parser.add_argument("--min-lines", type=int, default=0)
args = parser.parse_args()
for path in args.files:
n = len(path.read_text().splitlines())
if n >= args.min_lines:
if args.verbose:
print(f"{path}: {n} lines")
else:
print(n)
if __name__ == "__main__":
main()Edge Cases
An option value that itself looks like a flag (--output --verbose, where --verbose was meant as the filename) can confuse the parser — argparse generally handles this correctly by consuming exactly the values an option needs, but ambiguous cases are best avoided by users quoting or using = (--output=--verbose).
sys.argv[0] is the script path as invoked, not necessarily the script's actual location (it can be relative, or just the string used to launch it) — don't rely on it to locate other files; use __file__ or pathlib instead.
parser.parse_args() with no argument reads sys.argv[1:] automatically; passing an explicit list (as shown in several examples above) is mainly useful for testing argument parsing without actually running the program from a shell.
Best Practices
Prefer argparse over hand-rolled sys.argv parsing for anything beyond a single trivial argument. Give every argument a help= string — it's free documentation via --help. Use type= and choices= to validate early rather than discovering bad input deep in program logic. Use default= so callers aren't forced to specify every option. Group genuinely distinct actions into subcommands rather than a pile of mutually-exclusive flags.
Quick Reference
sys.argv raw list of argument strings, [0] = script name
parser.add_argument("name") positional, required
parser.add_argument("--opt") optional, takes a value
parser.add_argument("--flag", action="store_true") boolean flag
type=int / type=float / type=Path convert from string
choices=[...] restrict allowed values
default=... value used when omitted
required=True make an optional-style arg mandatory
nargs="+" / "*" one-or-more / zero-or-more values
action="append" accumulate repeated uses into a list
add_subparsers() git-style subcommandsKey Takeaways
sys.argv gives raw, unconverted, unvalidated argument strings straight from the shell.
argparse (standard library) turns argument declarations into parsing, type conversion, validation, and automatic --help/usage output.
Positional arguments are required and identified by order; options and flags are named and optional by default.
type=, choices=, default=, and required= cover most validation needs without manual checks.
Subcommands via add_subparsers() organize CLIs with multiple distinct actions cleanly.
import sys filename = sys.argv[1] # IndexError if the user forgets the argument import argparse parser = argparse.ArgumentParser() parser.add_argument("filename") args = parser.parse_args() # clear usage error if omitted, no traceback
Indexing sys.argv directly crashes with a raw IndexError and no guidance; argparse reports a proper usage message and exits cleanly.
parser.add_argument("--retries") args = parser.parse_args() total = args.retries + 1 # TypeError: can't add int and str parser.add_argument("--retries", type=int, default=3) args = parser.parse_args() total = args.retries + 1 # works: retries is already an int
Without type=, every argument value is a plain string; forgetting to convert it produces confusing type errors deep in the program instead of a clear parse-time error.
parser.add_argument("--verbose", default=True) # always truthy even when the flag is absent, and even a literal "--verbose false" would still be truthy since it's just a string parser.add_argument("--verbose", action="store_true") # True only if the flag is actually passed, False otherwise
For a boolean switch, action="store_true" is the correct tool; a plain default=True option would need explicit string-to-bool handling and misleads about intent.
What type are the elements of sys.argv, and what does sys.argv[0] represent?
All elements are strings; sys.argv[0] is the path/name the script was invoked with, and sys.argv[1:] are the actual arguments the user supplied.
What does argparse give you that raw sys.argv parsing does not?
Automatic type conversion, validation (choices, required), default values, generated --help/usage text, and consistent, non-crashing error messages for bad input.
What is the difference between a positional argument and an optional argument in argparse?
A positional argument (declared without leading dashes) is identified by its order and required by default; an optional argument (declared with -- or -x) is identified by name and optional unless required=True is set.
How do you declare a boolean command-line flag with argparse?
parser.add_argument('--verbose', action='store_true') — its value is True if the flag is present on the command line and False (or the given default) otherwise.
What happens if a user passes a value that isn't in an argument's choices list?
argparse automatically raises a usage error naming the invalid value and listing the allowed choices, without any manual validation code.
How would you let a CLI accept one or more filenames as positional arguments?
parser.add_argument('files', nargs='+') — this collects one or more values into a list on args.files, requiring at least one.
Why is passing an explicit list to parser.parse_args([...]) useful during development?
It lets you test argument parsing logic directly in code (e.g. in unit tests) without actually invoking the script from a shell with real sys.argv.
Write an argparse-based script that accepts a required positional 'path' argument, an optional --limit integer (default 10), and a --verbose flag.
import argparse def main() -> None: parser = argparse.ArgumentParser(description="Process a path.") parser.add_argument("path", help="path to process") parser.add_argument("--limit", type=int, default=10) parser.add_argument("--verbose", action="store_true") args = parser.parse_args() if args.verbose: print(f"Processing {args.path} with limit={args.limit}") if __name__ == "__main__": main() path is a required positional argument; --limit is an optional int-typed value defaulting to 10; --verbose is a boolean flag defaulting to False when absent.
What does this print, and what type is args.count?
It prints 10. args.count is an int (5), because type=int converts the raw string "5" before it's stored on the namespace, so args.count * 2 performs numeric multiplication rather than string repetition.
Write an argparse snippet for a repeatable --include flag that can be given multiple times and collects all values into a list, e.g. --include a.txt --include b.txt.
import argparse parser = argparse.ArgumentParser() parser.add_argument("--include", action="append", default=[]) args = parser.parse_args(["--include", "a.txt", "--include", "b.txt"]) print(args.include) # ['a.txt', 'b.txt'] action='append' accumulates each occurrence of --include into a list rather than overwriting a single value, and default=[] ensures args.include is an empty list (not None) when the flag is never given.
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--count", type=int, default=1)
args = parser.parse_args(["--count", "5"])
print(args.count * 2)