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 == False

Defaults

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 info

Type 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 subcommands

Key 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)