Marcus Kazmierczak

Home mkaz.blog

Working With Python

Python Command-Line Argument Parsing with Argparse

Python is my go-to tool for command-line scripts, which often require passing command-line arguments. This comprehensive guide serves as a reference for command-line argument parsing in Python, covering everything from basic sys.argv usage to advanced argparse features.

Quick Start / TL;DR

  • For simple scripts: Use sys.argv[1] to get the first argument
  • For robust CLI tools: Use argparse module (recommended)
  • Common pattern: Create ArgumentParser, add arguments, call parse_args()
  • Best practices: Always include help text, validate input types, handle errors gracefully
import argparse

# Basic argparse setup
parser = argparse.ArgumentParser(description='Your script description')
parser.add_argument('filename', help='Input file path')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose mode')
args = parser.parse_args()

print(f"Processing file: {args.filename}")
if args.verbose:
    print("Verbose mode enabled")

Common Questions

How do I parse command line arguments in Python?

Python offers several ways to parse command-line arguments, with argparse being the recommended approach for most cases:

import argparse

parser = argparse.ArgumentParser(description='Process some files')
parser.add_argument('files', nargs='+', help='Files to process')
parser.add_argument('--output', '-o', help='Output directory')
parser.add_argument('--verbose', '-v', action='store_true')

args = parser.parse_args()
print(f"Files to process: {args.files}")
print(f"Output directory: {args.output}")
print(f"Verbose mode: {args.verbose}")

What's the difference between argparse, sys.argv, and other parsing libraries?

Method Best For Pros Cons
sys.argv Simple scripts with 1-2 args Minimal code, direct access No validation, no help generation
argparse Most CLI applications Rich features, auto-help, validation More verbose setup
getopt Legacy compatibility Similar to C getopt More complex, less intuitive
click Complex CLI tools Decorators, subcommands External dependency

When should I use positional vs optional arguments?

Use positional arguments for:

  • Required inputs that are always needed
  • Arguments with a natural order (input file, output file)
  • Core functionality parameters

Use optional arguments for:

  • Configuration flags (--verbose, --debug)
  • Optional parameters with sensible defaults
  • Behavior modifiers
parser = argparse.ArgumentParser()
# Positional: required input file
parser.add_argument('input_file', help='File to process')
# Optional: configuration flags
parser.add_argument('--output', '-o', default='output.txt', help='Output file')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose mode')

How do I handle command line errors gracefully?

Argparse automatically handles many error cases, but you can add custom validation:

import argparse
import sys
from pathlib import Path

def validate_file(filepath):
    """Custom validation function"""
    path = Path(filepath)
    if not path.exists():
        raise argparse.ArgumentTypeError(f"File '{filepath}' does not exist")
    if not path.is_file():
        raise argparse.ArgumentTypeError(f"'{filepath}' is not a file")
    return path

parser = argparse.ArgumentParser()
parser.add_argument('input_file', type=validate_file, help='Input file')

try:
    args = parser.parse_args()
    print(f"Processing: {args.input_file}")
except SystemExit:
    # argparse calls sys.exit() on error
    print("Error: Invalid arguments provided")
    sys.exit(1)

Why choose argparse over getopt or sys.argv?

argparse advantages:

  • Automatic help generation (--help)
  • Type validation and conversion
  • Error handling with clear messages
  • Support for subcommands and complex argument structures
  • Consistent, pythonic API

When to consider alternatives:

  • sys.argv: Extremely simple scripts with minimal arguments
  • click: Complex CLI applications with subcommands and interactive features
  • getopt: When maintaining legacy code or need C-style compatibility

Basic Example

First, you may not need a module. If all you want to do is grab a single argument and no flags or other parameters passed in, use the sys.argv list that contains all of the command-line parameters.

The first element in sys.argv is the script's name. So a parameter passed in will be the second element: sys.argv[1]

import sys

if len(sys.argv) > 1:
    print(f"~ Script: {sys.argv[0]}")
    print(f"~ Arg   : {sys.argv[1]}")
else:
    print("No arguments")

Saving as test.py and running gives:

$ python test.py Foo
~ Script: test.py
~ Arg   : Foo

Multiple Arguments with sys.argv

Since sys.argv is simply a list, you can grab blocks of arguments together or slice around as you would any other list.

Last argument: sys.argv[-1]

All args after first: " ".join(sys.argv[2:])

Example with multiple arguments:

import sys

if len(sys.argv) > 1:
    print(f"Script: {sys.argv[0]}")
    print(f"All arguments: {sys.argv[1:]}")
    print(f"Last argument: {sys.argv[-1]}")
    print(f"Arguments 2-end: {' '.join(sys.argv[2:])}")
else:
    print("No arguments provided")

Output:

$ python test.py first second third
Script: test.py
All arguments: ['first', 'second', 'third']
Last argument: third
Arguments 2-end: second third

How to Use Argparse for Flag Parameters

Use argparse when you want to include flags (e.g., --help), handle optional arguments, or manage arguments with varying lengths. Argparse provides a robust framework for building command-line interfaces.

How to Create Boolean Flags (Help and Verbose Examples)

Boolean flags are one of the most common argument types. They're either present (True) or absent (False):

import argparse

parser = argparse.ArgumentParser(description='Demo script with verbose flag')
parser.add_argument('--verbose',
    action='store_true',
    help='Verbose mode')

args = parser.parse_args()

if args.verbose:
    print("~ Verbose mode!")
else:
    print("~ Running in normal mode")

Here's how to run the above example:

$ python test.py
~ Not so verbose

$ python test.py --verbose
~ Verbose!

The action parameter tells argparse to store True if the flag is found, otherwise it stores False. A great benefit of using argparse is the built-in help. Try it out by passing in an unknown parameter, -h or --help

$ python test.py --help
usage: test.py [-h] [--verbose]

Demo

optional arguments:
  -h, --help  show this help message and exit
    --verbose   verbose output

Extended help

If you want to add more information to the automatically generated help message use the epilog parameter when creating the ArgumentParser object.

parser = argparse.ArgumentParser(
    description='Demo',
    epilog="My extended help text"
)

By default, regardless of your formatting argparse will strip all whitespace in epilog and display as a single long string. If you want it to keep the whitespace, use the RawDescriptionHelpFormatter like so:

parser = argparse.ArgumentParser(
    description='Demo',
    formatter_class=argparse.RawDescriptionHelpFormatter,
    epilog="""
My longer text
    includes fancy
             whitespace formatting
    """
    )

Parameter error checking

A side effect of using argparse, you will get an error if a user passes in a command-line argument not expected, this includes flags or just an extra argument.

$ python test.py filename
usage: test.py [-h] [--verbose]
test.py: error: unrecognized arguments: filename

Multiple, Short or Long Flags

You can specify multiple flags for one argument, typically this is down with short and long flags, such as --verbose and -v

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--verbose', '-v',
    action='store_true',
    help='verbose flag' )

args = parser.parse_args()

if args.verbose:
    print("~ Verbose!")
else:
    print("~ Not so verbose")

Required Flags

You can make a flag required by setting, required=True this will cause an error if the flag is not specified.

parser = argparse.ArgumentParser()
parser.add_argument('--limit', required=True, type=int)
args = parser.parse_args()

Positional Arguments

The examples so far have been about flags, parameters starting with --, argparse also handles the positional args which are just specified without the flag. Here's an example to illustrate.

parser = argparse.ArgumentParser()
parser.add_argument('filename', help='Name of the file to process')
args = parser.parse_args()

print(f"~ Filename: {args.filename}")

Output:

$ python test.py filename.txt
~ Filename: filename.txt

Number of Arguments

Argparse determines the number of arguments based on the action specified, for our verbose example, the store_true action takes no argument. By default, argparse will look for a single argument, shown above in the filename example.

If you want your parameters to accept a list of items you can specify nargs=n for how many arguments to accept. Note, if you set nargs=1, it will return as a list not a single value.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('nums', nargs=2)
args = parser.parse_args()

print("~ Nums: {}".format(args.nums))

Output:

$ python test.py 5 2
~ Nums: ['5', '2']

Variable Number of Parameters

The nargs argument accepts a couple of extra special parameters. If you want the argument to accept all of the parameters, you can use * which will return all parameters if present, or empty list if none.

parser = argparse.ArgumentParser()
parser.add_argument('nums', nargs='*')
args = parser.parse_args()

print("~ Nums: {}".format(args.nums))

Output:

$ python test.py 5 2 4
~ Nums: ['5', '2', '4']

If you want to require, 1 or more parameters, use nargs='+'

Positional arguments are determined by the position specified. This can be combined with the nargs='*' for example if you want to define a filename and a list of values to store.

parser = argparse.ArgumentParser()
parser.add_argument('filename')
parser.add_argument('nums', nargs='*')
args = parser.parse_args()

print("~ Filename: {}".format(args.filename))
print("~ Nums: {}".format(args.nums))

Output:

$ python test.py file.txt 5 2 4
~ Fileanme: file.txt
~ Nums: ['5', '2', '4']

You can also specify nargs='?' if you want to make a positional argument optional, but you need to be careful how you combine ? and * parameters, especially if you put an optional positional parameter before another one.

This makes sense, not requiring the last args:

parser = argparse.ArgumentParser()
parser.add_argument('filename')
parser.add_argument('nums', nargs='?')
args = parser.parse_args()

Output:

$ python test.py test.txt 3
~ Filename: test.txt
~ Nums: 3

$ python test.py test.txt
~ Filename: test.txt
~ Nums: None

However, using the nargs='?' first will give unexpected results when arguments are missing, for example:

parser = argparse.ArgumentParser()
parser.add_argument('filename', nargs='?')
parser.add_argument('nums', nargs='*')
args = parser.parse_args()

Output:

$ python test.py 3 2 1
~ Filename: 3
~ Nums: ['2', '1']

You can use nargs with flag arguments as well.

parser = argparse.ArgumentParser()
parser.add_argument('--geo', nargs=2)
parser.add_argument('--pos', nargs=2)
parser.add_argument('type')
args = parser.parse_args()

Output:

$ python test.py --geo 5 10 --pos 100 50 square
~ Geo: ['5', '10']
~ Pos: ['100', '50']
~ Type: square

Variable Type

You might notice that the parameters passed in are being treated like strings and not numbers, you can specify the variable type by specifying type=int. By specifying the type, argparse will also fail if an invalid type is passed in.

parser = argparse.ArgumentParser()
parser.add_argument('nums', nargs=2, type=int)
args = parser.parse_args()

print("~ Nums: {}".format(args.nums))

Output:

$ python test.py 5 2
~ Nums: [5, 2]

File Types

Argparse has built-in filetypes that make it easier to open files specified on the command line. Here's an example of reading a file, you can do the same writing a file.

Traditional approach with argparse.FileType:

parser = argparse.ArgumentParser()
parser.add_argument('input_file', type=argparse.FileType('r'), help='Input file to read')
args = parser.parse_args()

for line in args.input_file:
    print(line.strip())
args.input_file.close()  # Don't forget to close

Modern approach with pathlib (recommended):

import argparse
from pathlib import Path

def validate_input_file(filepath):
    """Validate that the file exists and is readable"""
    path = Path(filepath)
    if not path.exists():
        raise argparse.ArgumentTypeError(f"File '{filepath}' does not exist")
    if not path.is_file():
        raise argparse.ArgumentTypeError(f"'{filepath}' is not a file")
    return path

parser = argparse.ArgumentParser()
parser.add_argument('input_file', type=validate_input_file, help='Input file to read')
args = parser.parse_args()

# Use pathlib's read_text() or open with context manager
content = args.input_file.read_text()
print(content)

# Or for line-by-line processing:
with args.input_file.open() as f:
    for line in f:
        print(line.strip())

Default Value

You may specify a default value if the user does not pass one in. Here's an example using a flag.

parser = argparse.ArgumentParser()
parser.add_argument('--limit', default=5, type=int)
args = parser.parse_args()

print("~ Limit: {}".format(args.limit))

Output:

$ python test.py
~ Limit: 5

Remainder

If you want to gather the extra arguments passed in, you can use remainder which gathers up all arguments not specified into a list.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--verbose',
    action='store_true',
    help='verbose flag' )
parser.add_argument('args', nargs=argparse.REMAINDER)
args = parser.parse_args()

print(args.args)

Specifying remainder will create a list of all remaining arguments:

$ python test.py --verbose foo bar
['foo', 'bar']

Actions

The default action is to assign the variable specified, but there are a couple of other actions that can be specified.

Booleans

We have already seen the boolean flag action which is action='store_true' which also has a counter action for action='store_false'

Count

You can use the count action, which will return how many times a flag was called, this can be useful for verbosity or silent flags.

parser = argparse.ArgumentParser()
parser.add_argument('--verbose', '-v', action='count')
args = parser.parse_args()

print("~ Verbose: {}".format(args.verbose))

Output:

$ python test.py
~ Verbose: None

$ python test.py --verbose
~ Verbose: 1

$ python test.py --verbose -v --verbose
~ Verbose: 3

Append

You can also use the append action to create a list if multiple flags are passed in.

parser = argparse.ArgumentParser()
parser.add_argument('-c', action='append')
args = parser.parse_args()

print("~ C: {}".format(args.c))

Output:

$ python test.py
~ C: None

$ python test.py -c hi
~ C: ['hi']

$ python test.py -c hi -c hello -c hey
~ C: ['hi', 'hello', 'hey']

Choices

If you only want a set of allowed values to be used, you can set the choices list, which will display an error if invalid entry.

parser = argparse.ArgumentParser(prog='roshambo.py')
parser.add_argument('throw', choices=['rock', 'paper', 'scissors'])
args = parser.parse_args()

print("~ Throw: {}".format(args.throw))

Examples

I'll end with two complete examples; many of the examples above are not as complete, they were kept short to focus on the idea being illustrated.

Copy Script Example

import argparse
import sys

parser = argparse.ArgumentParser(description='script to copy one file to another')

parser.add_argument('-v', '--verbose',
    action="store_true",
    help="verbose output" )

parser.add_argument('-R',
    action="store_false",
    help="Copy all files and directories recursively")

parser.add_argument('infile',
    type=argparse.FileType('r'),
    help="file to be copied")

parser.add_argument('outfile',
    type=argparse.FileType('w'),
    help="file to be created")

args = parser.parse_args()

Bug Script Example

Here is an example of a script that closes a bug

import argparse
import sys

parser = argparse.ArgumentParser(description='close bug')

parser.add_argument('-v', '--verbose',
    action="store_true",
    help="verbose output" )

parser.add_argument('-s',
    default="closed",
    choices=['closed', 'wontfix', 'notabug'],
    help="bug status")

parser.add_argument('bugnum',
    type=int,
    help="Bug number to be closed")

parser.add_argument('message',
    nargs='*',
    help="optional message")

args = parser.parse_args()

print(f"~ Bug Num: {args.bugnum}")
print(f"~ Verbose: {args.verbose}")
print(f"~ Status : {args.s}")
print(f"~ Message: {' '.join(args.message)}")

Advanced Usage and Best Practices

How to Handle Subcommands

For complex CLI tools, you might want to support subcommands—these are commands that perform different actions, similar to how git has subcommands like add and commit.

Argparse makes it easy to define these subcommands using the add_subparsers() method. Each subcommand can have its own set of arguments and help text, allowing you to build flexible, user-friendly command-line interfaces. The example below demonstrates how to set up a parser with two subcommands, add and commit, each with their own arguments.

import argparse

parser = argparse.ArgumentParser(description='Git-like tool')
subparsers = parser.add_subparsers(dest='command', help='Available commands')

# Add subparser
add_parser = subparsers.add_parser('add', help='Add files')
add_parser.add_argument('files', nargs='+', help='Files to add')

# Commit subparser
commit_parser = subparsers.add_parser('commit', help='Commit changes')
commit_parser.add_argument('-m', '--message', required=True, help='Commit message')

args = parser.parse_args()

if args.command == 'add':
    print(f"Adding files: {args.files}")
elif args.command == 'commit':
    print(f"Committing with message: {args.message}")
else:
    parser.print_help()

What if I need environment variable fallbacks?

import argparse
import os

parser = argparse.ArgumentParser()
parser.add_argument('--output-dir',
    default=os.environ.get('OUTPUT_DIR', './output'),
    help='Output directory (default: ./output or $OUTPUT_DIR)')

parser.add_argument('--api-key',
    default=os.environ.get('API_KEY'),
    help='API key (can be set via $API_KEY environment variable)')

args = parser.parse_args()

if not args.api_key:
    parser.error("API key is required. Set --api-key or $API_KEY environment variable")

Troubleshooting Common Argparse Issues

Common Error Messages and Solutions

Error: unrecognized arguments

script.py: error: unrecognized arguments: --typo

Solution: Check argument spelling, ensure all arguments are defined, or use parse_known_args() if you need to handle unknown arguments.

Error: the following arguments are required

script.py: error: the following arguments are required: filename

Solution: Either provide the required positional argument or make it optional with nargs='?' and a default value.

Error: argument --count: invalid int value

script.py: error: argument --count: invalid int value: 'abc'

Solution: Provide a valid integer, or add custom validation with better error messages.

Debugging Argparse Issues

1. Print parsed arguments for debugging:

args = parser.parse_args()
print(f"DEBUG: Parsed arguments: {vars(args)}")  # Convert namespace to dict

2. Use parse_known_args() for partial parsing:

args, unknown = parser.parse_known_args()
print(f"Known args: {args}")
print(f"Unknown args: {unknown}")

3. Test argument parsing in isolation:

import sys

# Save original sys.argv
original_argv = sys.argv.copy()

# Test with specific arguments
sys.argv = ['script.py', '--verbose', 'file.txt']
args = parser.parse_args()
print(args)

# Restore original
sys.argv = original_argv

Common "What If" Scenarios

What if I need to validate argument combinations?

parser = argparse.ArgumentParser()
parser.add_argument('--input-file')
parser.add_argument('--input-dir')
parser.add_argument('--recursive', action='store_true')

args = parser.parse_args()

# Custom validation after parsing
if not args.input_file and not args.input_dir:
    parser.error("Must specify either --input-file or --input-dir")

if args.recursive and not args.input_dir:
    parser.error("--recursive can only be used with --input-dir")

What if I need to modify arguments after parsing?

args = parser.parse_args()

# Convert relative paths to absolute
if args.input_file:
    args.input_file = os.path.abspath(args.input_file)

# Normalize and validate paths
if args.output_dir:
    args.output_dir = Path(args.output_dir).expanduser().resolve()
    args.output_dir.mkdir(parents=True, exist_ok=True)

What if I need to handle configuration files?

import argparse
import json
from pathlib import Path

def load_config(config_file):
    """Load configuration from JSON file"""
    if config_file and Path(config_file).exists():
        with open(config_file) as f:
            return json.load(f)
    return {}

parser = argparse.ArgumentParser()
parser.add_argument('--config', help='Configuration file path')
parser.add_argument('--verbose', action='store_true')
parser.add_argument('--output-dir', default='./output')

# Parse args once to get config file
args, remaining = parser.parse_known_args()

# Load config file and set defaults
config = load_config(args.config)
parser.set_defaults(**config)

# Parse again with config defaults
args = parser.parse_args(remaining)

Best Practices Summary

  1. Always provide help text for every argument
  2. Use type validation (type=int, custom validators) rather than manual conversion
  3. Validate argument combinations after parsing when needed
  4. Use pathlib for file/directory handling
  5. Provide sensible defaults and document them
  6. Handle errors gracefully with clear error messages
  7. Use environment variables as fallbacks when appropriate
  8. Test edge cases like missing files, invalid input, empty arguments

Resources