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. The description parameter sets the text that appears at the top of the auto-generated help output, which is the first thing users see when they run --help. Every argument you add with add_argument becomes an attribute on the Namespace object that parse_args() returns, using the argument’s long name (minus the leading dashes) as the attribute name.
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 to see how argparse translates command-line tokens into Python values. A positional argument like filename is required and matched by position, while optional flags like --verbose are matched by name and can appear anywhere in the argument list before or after the positional argument.
$ 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 — the first positional argument added to the parser receives the first command-line value, the second receives the second value, and so on. The example below shows two positional arguments for input and output file paths.
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 before your code ever sees it. This is one of argparse’s most useful features because it catches type errors early with a meaningful error message instead of letting them surface as confusing tracebacks deep in your application logic. The example below converts the first positional argument to int and the second to float.
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 and exits with a non-zero exit code. This means you don’t need to write your own validation logic for type mismatches — argparse handles it for you, including printing the usage line and a descriptive error message. The output below shows what happens when you pass a string where an integer is expected.
$ 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. They are optional by default, meaning the script runs without them and you get either None or the default value you specify. Short flags are useful for frequently-used options, while long flags make scripts self-documenting when read by someone who hasn’t memorized every single-letter flag. The example below shows both forms for the --name argument and a default of 1 for --count.
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 for conciseness when invoking the script from the terminal. The command below is equivalent to using separate --name and --count flags with their long forms but saves typing for repeated interactive use. Combining short flags is a Unix convention that argparse supports automatically.
$ 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 — the mere presence of the flag on the command line sets the attribute to True. This is the most common pattern for toggles like --verbose or --debug. If the flag is absent, the attribute defaults to False. The example below defines two boolean flags and shows that only the one explicitly passed is set to True.
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 becomes False when the flag appears on the command line. This is useful for flags like --no-color where the default behavior is to use color and the flag turns it off. Note that the attribute name loses the no- prefix, so --no-color becomes args.no_color.
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 with the choices parameter. Argparse checks the value against the allowed set at parse time and rejects anything outside it with an error message that lists the valid options. This is much cleaner than accepting any string and then validating it manually inside your application code, because the user sees the error immediately rather than after some processing has already happened.
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 a choices that uses Python’s range object. This constrains the argument to a specific integer interval and produces a clear error if the user provides a value outside that range. The example below accepts port numbers between 1024 and 65535 inclusive.
parser.add_argument('--port', type=int, choices=range(1024, 65536))
# Only accepts 1024-65535
Required options
By default, optional arguments are genuinely optional — the script runs without them and their value is None or the default. To make one required, pass required=True. This is less common than providing a sensible default, but it makes sense for arguments like --config where there is no meaningful fallback value and the script cannot do anything useful without it.
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. The default parameter works for both optional and positional arguments, though it’s most commonly used with optional ones since positional arguments are required by default and don’t typically have defaults. Specifying sensible defaults means your CLI is usable with zero configuration and progressively more powerful as the user adds flags.
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.” With this setting, the argument accepts zero or one value. When the flag is absent, the default value is used. When the flag is present without a value, the const value takes effect. When the flag is present with a value, that value is used directly. This three-way behavior is useful for arguments like --mode where you want different behavior depending on whether the user passed nothing, just the flag, or the flag with a specific option.
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. The difference matters: nargs='+' requires at least one value and produces an error if none are given, while nargs='*' accepts zero values gracefully and returns an empty list. Both produce a Python list as the parsed value, so you can iterate over the results with a for loop or check len() regardless of which nargs variant you chose.
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. Every argument should have a help string because --help is often the first interaction a new user has with your tool and a well-written help message can be the difference between someone figuring out your CLI in seconds versus giving up. The description and epilog parameters on the ArgumentParser constructor control text at the top and bottom of the help output respectively.
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 output that lists every argument with its help text, grouped by type into positional arguments and options. The --help flag itself is added automatically by argparse, along with the -h shorthand, so you never need to define it yourself.
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 to enforce that only one of them can be used at a time. Argparse automatically generates the --help output with a vertical bar separating the exclusive options and rejects any command line that tries to use more than one. This is the standard way to implement flags like --short and --long that represent conflicting output formats or modes of operation.
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 a clear error with the usage line showing the exclusive relationship. The error message names both arguments so the user can immediately see which flags conflict, without needing to read the full help text.
$ 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 instead of relying on sys.argv. This lets you write unit tests for your argument parsing logic without spawning subprocesses or manipulating the actual command-line arguments of the test runner. You can construct argument lists programmatically and verify that the resulting Namespace object has the expected attributes and values.
args = parser.parse_args(['--name', 'Alice', 'input.txt'])
# Does not touch sys.argv
This makes it easy to test your argument parsing logic in isolation, without side effects on the global process state.
A complete example
The following example combines positional arguments, boolean flags, and file I/O into a grep-like search tool. It demonstrates the full argparse workflow: defining the parser, adding multiple argument types, parsing the command line, and then using the parsed values to drive application logic. The --ignore-case flag toggles a regex compilation option, while --line-number and --count control output formatting.
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. This means you can safely access args.name without an AttributeError, but you should check for None explicitly if the argument is truly optional and your code cannot proceed without it.
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 clear but basic. For more sophisticated validation, you can pass a custom function to type that raises argparse.ArgumentTypeError with a descriptive message tailored to your domain. The built-in behavior shown below is usually sufficient for simple numeric arguments.
# 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 parameter replaces the value when the argument is absent, but note that the default must be a list because nargs produces a list. An empty list [] is the most common default, but you could also provide a pre-populated list. When the argument appears, the default is overridden with whatever values the user provides.
parser.add_argument('--files', nargs='*', default=[])
# --files not provided: args.files = []
# --files a b c: args.files = ['a', 'b', 'c']
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.
See also
- subprocess guide — run external commands from Python
- env variables — handle environment variables in CLIs
- argparse guide — deeper dive into argparse patterns