Skip to main content

argparse.Action

The Action class is the base class for all action classes in argparse. Actions define how command-line arguments are processed and stored when they are encountered during parsing. Understanding actions is crucial for creating custom argument behavior.

📚 Quick Reference

Module: argparse
Type: Base Class
Purpose: Define how command-line arguments are processed and stored
Documentation: argparse.Action
Inheritance: Subclass to create custom actions

🔧 Action Base Class

Constructor

argparse.Action(option_strings, dest, nargs=None, const=None, default=None, 
type=None, choices=None, required=False, help=None, metavar=None)

Parameters:

  • option_strings: List of command-line option strings (e.g., ['-f', '--foo'])
  • dest: Attribute name to store parsed values
  • nargs: Number of command-line arguments to consume
  • const: Constant value for certain action types
  • default: Default value when argument is not present
  • type: Function to convert argument string to desired type
  • choices: Allowed values for the argument
  • required: Whether argument is required (for optional arguments)
  • help: Help message for the argument
  • metavar: Name for argument in help messages

Core Methods

__call__(parser, namespace, values, option_string=None)

Called when the action processes an argument.

Parameters:

  • parser: ArgumentParser instance processing the arguments
  • namespace: Namespace object to store parsed values
  • values: Associated command-line arguments
  • option_string: Option string that triggered this action

Must be implemented by subclasses.

🎯 Built-in Action Types

1. Store Actions

store (default)

Store the argument value.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--name', action='store') # Default action
parser.add_argument('--age', type=int) # Implicit store action

args = parser.parse_args(['--name', 'Alice', '--age', '25'])
print(args.name) # 'Alice'
print(args.age) # 25

store_const

Store a constant value.

parser = argparse.ArgumentParser()
parser.add_argument('--verbose', action='store_const', const=True)
parser.add_argument('--quiet', action='store_const', const=True, dest='verbose')

args = parser.parse_args(['--verbose'])
print(args.verbose) # True

args = parser.parse_args(['--quiet'])
print(args.verbose) # True (same destination)

store_true / store_false

Store True/False when the argument is present.

parser = argparse.ArgumentParser()
parser.add_argument('--verbose', action='store_true')
parser.add_argument('--quiet', action='store_false')

args = parser.parse_args(['--verbose'])
print(args.verbose) # True
print(args.quiet) # False (default)

args = parser.parse_args(['--quiet'])
print(args.verbose) # False (default)
print(args.quiet) # False (explicit)

2. Append Actions

append

Append values to a list.

parser = argparse.ArgumentParser()
parser.add_argument('--item', action='append')

args = parser.parse_args(['--item', 'a', '--item', 'b', '--item', 'c'])
print(args.item) # ['a', 'b', 'c']

append_const

Append a constant value to a list.

parser = argparse.ArgumentParser()
parser.add_argument('--verbose', action='append_const', const='v', dest='flags')
parser.add_argument('--debug', action='append_const', const='d', dest='flags')

args = parser.parse_args(['--verbose', '--debug', '--verbose'])
print(args.flags) # ['v', 'd', 'v']

3. Count Action

count

Count the number of times an argument appears.

parser = argparse.ArgumentParser()
parser.add_argument('-v', '--verbose', action='count', default=0)

args = parser.parse_args(['-v'])
print(args.verbose) # 1

args = parser.parse_args(['-vvv'])
print(args.verbose) # 3

args = parser.parse_args(['--verbose', '--verbose'])
print(args.verbose) # 2

4. Special Actions

help

Show help message and exit.

parser = argparse.ArgumentParser()
parser.add_argument('-h', '--help', action='help') # Usually automatic

version

Show version information and exit.

parser = argparse.ArgumentParser()
parser.add_argument('--version', action='version', version='MyApp 1.0')

# Running with --version prints "MyApp 1.0" and exits

extend (Python 3.8+)

Extend a list with multiple values.

parser = argparse.ArgumentParser()
parser.add_argument('--items', action='extend', nargs='+')

args = parser.parse_args(['--items', 'a', 'b', '--items', 'c', 'd'])
print(args.items) # ['a', 'b', 'c', 'd']

🛠️ Custom Actions

Creating Custom Actions

import argparse

class ValidateEmailAction(argparse.Action):
"""Custom action to validate email addresses."""

def __call__(self, parser, namespace, values, option_string=None):
import re

email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

if isinstance(values, list):
# Handle multiple values
for email in values:
if not re.match(email_pattern, email):
parser.error(f"Invalid email address: {email}")
setattr(namespace, self.dest, values)
else:
# Handle single value
if not re.match(email_pattern, values):
parser.error(f"Invalid email address: {values}")
setattr(namespace, self.dest, values)

# Usage
parser = argparse.ArgumentParser()
parser.add_argument('--email', action=ValidateEmailAction)
parser.add_argument('--emails', action=ValidateEmailAction, nargs='+')

# Valid usage
args = parser.parse_args(['--email', 'user@example.com'])
print(args.email) # user@example.com

# Invalid usage would raise an error
# args = parser.parse_args(['--email', 'invalid-email'])

File Validation Action

import os
import argparse

class ValidateFileAction(argparse.Action):
"""Custom action to validate file existence and permissions."""

def __init__(self, option_strings, dest, **kwargs):
self.must_exist = kwargs.pop('must_exist', True)
self.readable = kwargs.pop('readable', False)
self.writable = kwargs.pop('writable', False)
super().__init__(option_strings, dest, **kwargs)

def __call__(self, parser, namespace, values, option_string=None):
filepath = values

if self.must_exist and not os.path.exists(filepath):
parser.error(f"File does not exist: {filepath}")

if os.path.exists(filepath):
if self.readable and not os.access(filepath, os.R_OK):
parser.error(f"File is not readable: {filepath}")

if self.writable and not os.access(filepath, os.W_OK):
parser.error(f"File is not writable: {filepath}")

setattr(namespace, self.dest, filepath)

# Usage
parser = argparse.ArgumentParser()
parser.add_argument('--input', action=ValidateFileAction,
must_exist=True, readable=True)
parser.add_argument('--output', action=ValidateFileAction,
must_exist=False, writable=True)

args = parser.parse_args(['--input', 'existing_file.txt',
'--output', 'output_file.txt'])

Range Validation Action

class RangeAction(argparse.Action):
"""Custom action to validate numeric ranges."""

def __init__(self, option_strings, dest, min_val=None, max_val=None, **kwargs):
self.min_val = min_val
self.max_val = max_val
super().__init__(option_strings, dest, **kwargs)

def __call__(self, parser, namespace, values, option_string=None):
try:
# Convert to appropriate numeric type
if self.type:
value = self.type(values)
else:
# Try int first, then float
try:
value = int(values)
except ValueError:
value = float(values)

# Validate range
if self.min_val is not None and value < self.min_val:
parser.error(f"{option_string} must be >= {self.min_val}")

if self.max_val is not None and value > self.max_val:
parser.error(f"{option_string} must be <= {self.max_val}")

setattr(namespace, self.dest, value)

except ValueError:
parser.error(f"{option_string} must be a number")

# Usage
parser = argparse.ArgumentParser()
parser.add_argument('--port', action=RangeAction, min_val=1, max_val=65535)
parser.add_argument('--percentage', action=RangeAction, min_val=0.0, max_val=100.0)

args = parser.parse_args(['--port', '8080', '--percentage', '75.5'])
print(args.port) # 8080
print(args.percentage) # 75.5

Multiple Choice Action

class MultipleChoiceAction(argparse.Action):
"""Action that allows multiple selections from a set of choices."""

def __init__(self, option_strings, dest, choices=None, **kwargs):
if choices is None:
choices = []
self.available_choices = set(choices)
kwargs['choices'] = None # Don't let argparse handle choices
super().__init__(option_strings, dest, **kwargs)

def __call__(self, parser, namespace, values, option_string=None):
if isinstance(values, str):
values = [values]

# Validate all choices
invalid_choices = set(values) - self.available_choices
if invalid_choices:
parser.error(f"Invalid choices: {invalid_choices}. "
f"Choose from: {self.available_choices}")

# Store unique values
current_values = getattr(namespace, self.dest, []) or []
current_values.extend(values)
setattr(namespace, self.dest, list(set(current_values)))

# Usage
parser = argparse.ArgumentParser()
parser.add_argument('--features', action=MultipleChoiceAction,
choices=['auth', 'cache', 'logging', 'metrics'],
help='Enable features (can be specified multiple times)')

args = parser.parse_args(['--features', 'auth', '--features', 'cache'])
print(args.features) # ['auth', 'cache']

🎯 Advanced Action Patterns

Conditional Actions

class ConditionalAction(argparse.Action):
"""Action that behaves differently based on other arguments."""

def __init__(self, option_strings, dest, condition_arg=None, **kwargs):
self.condition_arg = condition_arg
super().__init__(option_strings, dest, **kwargs)

def __call__(self, parser, namespace, values, option_string=None):
# Check condition
condition_value = getattr(namespace, self.condition_arg, None)

if condition_value == 'verbose':
# Store detailed information
setattr(namespace, self.dest, f"Detailed: {values}")
elif condition_value == 'quiet':
# Store minimal information
setattr(namespace, self.dest, values[:10] + "...")
else:
# Default behavior
setattr(namespace, self.dest, values)

# Usage
parser = argparse.ArgumentParser()
parser.add_argument('--mode', choices=['verbose', 'quiet', 'normal'])
parser.add_argument('--message', action=ConditionalAction,
condition_arg='mode')

args = parser.parse_args(['--mode', 'verbose', '--message', 'Hello World'])
print(args.message) # "Detailed: Hello World"

Accumulating Actions

class KeyValueAction(argparse.Action):
"""Action to parse key=value pairs into a dictionary."""

def __call__(self, parser, namespace, values, option_string=None):
# Initialize dictionary if needed
if not hasattr(namespace, self.dest) or getattr(namespace, self.dest) is None:
setattr(namespace, self.dest, {})

# Parse key=value
try:
key, value = values.split('=', 1)
getattr(namespace, self.dest)[key] = value
except ValueError:
parser.error(f"'{values}' is not in key=value format")

# Usage
parser = argparse.ArgumentParser()
parser.add_argument('-D', '--define', action=KeyValueAction,
help='Define key=value pairs')

args = parser.parse_args(['-D', 'name=Alice', '-D', 'age=25', '-D', 'city=NYC'])
print(args.define) # {'name': 'Alice', 'age': '25', 'city': 'NYC'}

🎯 Practical Use Cases

1. Configuration File Action

import json
import argparse

class ConfigFileAction(argparse.Action):
"""Load configuration from JSON file."""

def __call__(self, parser, namespace, values, option_string=None):
try:
with open(values, 'r') as f:
config = json.load(f)

# Merge config into namespace
for key, value in config.items():
setattr(namespace, key, value)

# Store config file path
setattr(namespace, self.dest, values)

except FileNotFoundError:
parser.error(f"Config file not found: {values}")
except json.JSONDecodeError as e:
parser.error(f"Invalid JSON in config file: {e}")

# Usage
parser = argparse.ArgumentParser()
parser.add_argument('--config', action=ConfigFileAction)
parser.add_argument('--name', default='DefaultName')
parser.add_argument('--port', type=int, default=8080)

# With config.json: {"name": "MyApp", "port": 3000}
args = parser.parse_args(['--config', 'config.json'])
print(args.name) # "MyApp" (from config)
print(args.port) # 3000 (from config)
print(args.config) # "config.json"

2. Environment Variable Action

import os

class EnvVarAction(argparse.Action):
"""Use environment variable as default value."""

def __init__(self, option_strings, dest, env_var=None, **kwargs):
self.env_var = env_var or dest.upper()

# Set default from environment variable
if self.env_var in os.environ:
kwargs['default'] = os.environ[self.env_var]

super().__init__(option_strings, dest, **kwargs)

def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)

# Usage
parser = argparse.ArgumentParser()
parser.add_argument('--host', action=EnvVarAction, env_var='SERVER_HOST',
default='localhost')
parser.add_argument('--port', action=EnvVarAction, env_var='SERVER_PORT',
type=int, default=8080)

# Environment variables: SERVER_HOST=example.com, SERVER_PORT=3000
args = parser.parse_args([])
print(args.host) # "example.com" (from env var)
print(args.port) # 3000 (from env var)

# Command line overrides environment
args = parser.parse_args(['--host', 'override.com'])
print(args.host) # "override.com" (from command line)

3. Validation and Transformation Action

import re
from urllib.parse import urlparse

class URLAction(argparse.Action):
"""Validate and normalize URL arguments."""

def __call__(self, parser, namespace, values, option_string=None):
# Add protocol if missing
if not values.startswith(('http://', 'https://')):
values = 'https://' + values

# Parse and validate URL
try:
parsed = urlparse(values)
if not all([parsed.scheme, parsed.netloc]):
raise ValueError("Invalid URL structure")

# Normalize URL
normalized_url = parsed.geturl()
setattr(namespace, self.dest, normalized_url)

except Exception as e:
parser.error(f"Invalid URL '{values}': {e}")

# Usage
parser = argparse.ArgumentParser()
parser.add_argument('--url', action=URLAction)

args = parser.parse_args(['--url', 'example.com/path'])
print(args.url) # "https://example.com/path"

args = parser.parse_args(['--url', 'http://example.com'])
print(args.url) # "http://example.com"

🚀 Performance Considerations

Lazy Validation

class LazyValidationAction(argparse.Action):
"""Defer expensive validation until all arguments are parsed."""

def __init__(self, option_strings, dest, validator=None, **kwargs):
self.validator = validator
super().__init__(option_strings, dest, **kwargs)

def __call__(self, parser, namespace, values, option_string=None):
# Store raw value for now
setattr(namespace, self.dest, values)

# Store validator for later execution
if not hasattr(namespace, '_validators'):
namespace._validators = []

namespace._validators.append((self.dest, self.validator, values))

def validate_deferred(namespace):
"""Run deferred validations."""
if not hasattr(namespace, '_validators'):
return

for dest, validator, value in namespace._validators:
if validator and not validator(value):
raise ValueError(f"Validation failed for {dest}: {value}")

# Usage example with expensive validation
def expensive_validator(value):
# Simulate expensive operation
import time
time.sleep(0.1)
return len(value) > 5

parser = argparse.ArgumentParser()
parser.add_argument('--name', action=LazyValidationAction,
validator=expensive_validator)

args = parser.parse_args(['--name', 'Alice'])
# Validation happens here
validate_deferred(args)

🔍 Testing Custom Actions

import unittest
import argparse
from io import StringIO
import sys

class TestCustomActions(unittest.TestCase):
"""Test custom argparse actions."""

def test_email_validation_action(self):
"""Test email validation action."""
parser = argparse.ArgumentParser()
parser.add_argument('--email', action=ValidateEmailAction)

# Valid email
args = parser.parse_args(['--email', 'test@example.com'])
self.assertEqual(args.email, 'test@example.com')

# Invalid email should raise SystemExit
with self.assertRaises(SystemExit):
parser.parse_args(['--email', 'invalid-email'])

def test_range_action(self):
"""Test range validation action."""
parser = argparse.ArgumentParser()
parser.add_argument('--port', action=RangeAction,
min_val=1, max_val=65535)

# Valid port
args = parser.parse_args(['--port', '8080'])
self.assertEqual(args.port, 8080)

# Invalid port (too high)
with self.assertRaises(SystemExit):
parser.parse_args(['--port', '99999'])

def test_key_value_action(self):
"""Test key-value parsing action."""
parser = argparse.ArgumentParser()
parser.add_argument('-D', action=KeyValueAction)

args = parser.parse_args(['-D', 'key=value'])
self.assertEqual(args.D, {'key': 'value'})

if __name__ == '__main__':
unittest.main()
  • argparse.ArgumentParser - Main parser class that uses actions
  • argparse.Namespace - Container for parsed argument values
  • argparse.FileType - Built-in action for file handling

📚 Additional Resources

Custom actions provide powerful ways to extend argparse functionality, enabling sophisticated argument validation, transformation, and processing that goes beyond the built-in action types.