pyguides

argparse Basics: Parsing Command-Line Arguments in Python

Intro context

argparse basics are the foundation of almost every Python CLI you’ve used. The argparse module lives in the standard library and handles the tedious work of parsing command-line arguments, generating --help output, and reporting usage errors with a non-zero exit code. Most Python CLIs you run (pip, pytest, black) use argparse under the hood, so learning argparse basics pays off whether you’re writing your own tool or reading someone else’s.

This guide walks through creating an ArgumentParser, adding positional and optional arguments, converting types, setting defaults, using action='store_true' for flags, handling unused flags that return None, and reading what argparse-generated help text looks like. For more advanced patterns once these argparse basics click, see the argparse guide and the Click framework tutorial.

Creating a parser

Start by importing argparse and creating an ArgumentParser object:

import argparse

parser = argparse.ArgumentParser(description="Read and print a file")
parser.add_argument('filename')
parser.add_argument('--verbose', '-v', action='store_true')
args = parser.parse_args()

print(args.filename)
print("Verbose:", args.verbose)

Run it:

$ python cli.py myfile.txt
myfile.txt
Verbose: False

$ python cli.py --verbose myfile.txt
myfile.txt
Verbose: True

parse_args() reads sys.argv[1:] by default and returns a Namespace object. Attributes on that object match the argument names you defined.

Positional arguments

Positional arguments come before the dashes and are required by default. The order matters:

parser = argparse.ArgumentParser()
parser.add_argument('input_file')
parser.add_argument('output_file')
args = parser.parse_args()
# python script.py data.csv result.csv
# args.input_file = "data.csv"
# args.output_file = "result.csv"

You can add type conversion so parse_args() validates and converts the value:

parser.add_argument('count', type=int)
parser.add_argument('rate', type=float)
args = parser.parse_args(['100', '0.5'])
# args.count = 100 (int)
# args.rate = 0.5 (float)

If the user passes an invalid type, argparse prints an error automatically:

$ python script.py abc
usage: script.py [-h] count
script.py: error: argument count: invalid int value: 'abc'

Optional arguments with flags

Optional arguments start with - for short flags and -- for long flags:

parser.add_argument('-n', '--name', default='World')
parser.add_argument('--count', type=int, default=1)
args = parser.parse_args([])
# args.name = 'World'
# args.count = 1

args = parser.parse_args(['--name', 'Alice', '--count', '5'])
# args.name = 'Alice'
# args.count = 5

Short flags can be combined:

$ python script.py -n Bob --count 3

Boolean flags (store_true / store_false)

Use action='store_true' for flags that don’t need a value:

parser.add_argument('-v', '--verbose', action='store_true')
parser.add_argument('-q', '--quiet', action='store_true')
args = parser.parse_args(['-v'])
# args.verbose = True
# args.quiet = False

action='store_false' is the inverse , the attribute is True by default and False when the flag appears:

parser.add_argument('--no-color', action='store_false')
parser.add_argument('--highlight', action='store_true', default=False)
# With --no-color on the command line:
# args.highlight = False  (default)
# args.no_color = False   (because --no-color was given)

Choices and validation

Restrict an argument to specific values:

parser.add_argument('--format', choices=['json', 'yaml', 'csv'])
args = parser.parse_args(['--format', 'json'])
# args.format = 'json'

# Invalid choice:
# $ python script.py --format xml
# error: argument --format: invalid choice: 'xml'

For numeric ranges, combine type=int with explicit validation:

parser.add_argument('--port', type=int, choices=range(1024, 65536))
# Only accepts 1024-65535

Required options

By default, optional arguments are , optional. To make one required:

parser.add_argument('--config', required=True)

This is relatively rare. Most of the time you want a sensible default instead.

Default values

Set defaults so the script runs without every argument being specified:

parser.add_argument('--output', default='output.txt')
parser.add_argument('--lines', type=int, default=10)
args = parser.parse_args([])
# args.output = 'output.txt'
# args.lines = 10

Use nargs='?' to distinguish between “not provided” and “explicitly set to default”:

parser.add_argument('--mode', nargs='?', default='auto', const='forced')
# No flag: args.mode = 'auto'
# --mode: args.mode = 'forced'
# --mode custom: args.mode = 'custom'

Counting values

Use nargs='+' to accept one or more values, or nargs='*' for zero or more:

parser.add_argument('files', nargs='+')
parser.add_argument('--tags', nargs='*', default=[])
args = parser.parse_args(['file1.txt', 'file2.txt', '--tags', 'urgent', 'review'])
# args.files = ['file1.txt', 'file2.txt']
# args.tags = ['urgent', 'review']

Help messages and —help

Add help= to make your CLI self-documenting:

parser = argparse.ArgumentParser(
    description="Copy a file to a destination",
    epilog="Report bugs to dev@example.com"
)
parser.add_argument('source', help="Path to the source file")
parser.add_argument('dest', help="Path to the destination")
parser.add_argument('--verbose', '-v', action='store_true', help="Print progress")
args = parser.parse_args()

Running with -h or --help produces:

usage: cp.py [-h] [--verbose] source dest

Copy a file to a destination

positional arguments:
  source      Path to the source file
  dest        Path to the destination

options:
  -h, --help  show this help message and exit
  --verbose, -v  Print progress

The --help flag is added automatically. argparse also adds -h as a shorthand.

Mutual exclusion

Put mutually exclusive arguments in a group:

group = parser.add_mutually_exclusive_group()
group.add_argument('--short', action='store_true')
group.add_argument('--long', action='store_true')

Using both --short and --long together produces an error:

$ python script.py --short --long
usage: script.py [-h] [--short | --long]
script.py: error: argument --long: not allowed with argument --short

Parsing without sys.argv

For testing or scripted use, pass a list directly to parse_args:

args = parser.parse_args(['--name', 'Alice', 'input.txt'])
# Does not touch sys.argv

This makes it easy to test your argument parsing logic.

A complete example

import argparse

def main():
    parser = argparse.ArgumentParser(description="Grep-like text search")
    parser.add_argument('pattern', help="Regex pattern to search for")
    parser.add_argument('files', nargs='+', help="Files to search")
    parser.add_argument('-i', '--ignore-case', action='store_true',
                        help="Case-insensitive matching")
    parser.add_argument('-n', '--line-number', action='store_true',
                        help="Show line numbers")
    parser.add_argument('-c', '--count', action='store_true',
                        help="Show only the count of matches per file")

    args = parser.parse_args()

    import re
    flags = re.IGNORECASE if args.ignore_case else 0
    pattern = re.compile(args.pattern, flags)

    for filepath in args.files:
        try:
            with open(filepath) as f:
                for lineno, line in enumerate(f, 1):
                    if pattern.search(line):
                        if args.count:
                            continue  # just count for now
                        prefix = f"{filepath}:" if len(args.files) > 1 else ""
                        num_prefix = f"{lineno}:" if args.line_number else ""
                        print(f"{prefix}{num_prefix}{line.rstrip()}")
        except FileNotFoundError:
            print(f"Skipping {filepath}: not found")

if __name__ == '__main__':
    main()

Common pitfalls

Getting none when the flag wasn’t used

Attributes always exist on the Namespace object, even if the flag wasn’t used. They’re None or the default value, not missing:

parser.add_argument('--name')
args = parser.parse_args([])
print(args.name)  # None, not AttributeError

Type errors happen at parse time

If you pass type=int, the error message is cryptic by default. Consider wrapping or using argparse.FileType for files:

# This fails with a clear message:
parser.add_argument('port', type=int)
# $ python script.py abc
# error: argument port: invalid int value: 'abc'

Combining nargs and default

With nargs='*' or nargs='+', the default replaces an empty list, not None:

parser.add_argument('--files', nargs='*', default=[])
# --files not provided: args.files = []
# --files a b c: args.files = ['a', 'b', 'c']

See also

Where to go next

These argparse basics get you a working CLI. For deeper coverage, see the argparse guide for subparsers and groups, the Click framework tutorial for a friendlier third-party option, and the Typer framework tutorial for argparse-on-type-hints.