Contents Menu Expand Light mode Dark mode Auto light/dark, in light mode Auto light/dark, in dark mode Skip to content
argclass
Light Logo Dark Logo

Getting Started

  • Quick Start
  • Tutorial
  • Examples Gallery

User Guide

  • Arguments
  • Argument Groups
  • Subparsers
  • Configuration Files
  • Environment Variables
  • Secrets
  • Security Checklist
  • Third-Party Integrations

Help

  • Error Handling
  • Common Pitfalls

Reference

  • API Reference
Back to top
View this page
Edit this page

Examples Gallery¶

Copy-pastable examples for common CLI patterns. Each example is self-contained and demonstrates specific argclass features you can adapt for your own projects.

Simple CLI Tool¶

This example shows the fundamental building blocks of any CLI application. It demonstrates how to create a parser class with different argument types: a required positional-style argument, an optional argument with a default, and a boolean flag.

Key features demonstrated:

  • Required arguments (name: str)

  • Optional arguments with defaults (count: int = 1)

  • Boolean flags (loud: bool = False)

  • Short aliases (-c for --count)

  • Implementing __call__ for executable parsers

import argclass

class Greeter(argclass.Parser):
    """Greet someone."""
    name: str = argclass.Argument(help="Name to greet")
    count: int = argclass.Argument("-c", "--count", default=1, help="Times to greet")
    loud: bool = False

    def __call__(self) -> int:
        greeting = f"Hello, {self.name}!"
        if self.loud:
            greeting = greeting.upper()
        for _ in range(self.count):
            print(greeting)
        return 0

greeter = Greeter()
greeter.parse_args(["--name", "World", "-c", "2", "--loud"])
assert greeter.name == "World"
assert greeter.count == 2
assert greeter.loud is True

Subcommand CLI (git-style)¶

Many professional CLI tools use subcommands to organize functionality: git commit, docker run, kubectl apply. This pattern makes complex tools intuitive by grouping related operations under descriptive command names.

This example shows how to build a project management tool with init, build, and deploy subcommands. Each subcommand is a separate parser class with its own arguments, while the parent parser holds global options like --verbose.

Key features demonstrated:

  • Subcommand pattern for multi-command CLIs

  • Global options available to all subcommands

  • choices parameter for constrained values

  • Path type for file system arguments

  • Each subcommand as a callable with its own __call__ method

import argclass
from pathlib import Path

class InitCommand(argclass.Parser):
    """Initialize a new project."""
    name: str = argclass.Argument(help="Project name")
    template: str = argclass.Argument(default="basic", choices=["basic", "full"])

    def __call__(self) -> int:
        print(f"Initializing {self.name} with {self.template} template")
        return 0

class BuildCommand(argclass.Parser):
    """Build the project."""
    output: Path = argclass.Argument("-o", "--output", default=Path("dist"))
    release: bool = False

    def __call__(self) -> int:
        mode = "release" if self.release else "debug"
        print(f"Building to {self.output} ({mode})")
        return 0

class DeployCommand(argclass.Parser):
    """Deploy to production."""
    target: str = argclass.Argument(choices=["staging", "production"])
    dry_run: bool = False

    def __call__(self) -> int:
        if self.dry_run:
            print(f"Would deploy to {self.target}")
        else:
            print(f"Deploying to {self.target}")
        return 0

class CLI(argclass.Parser):
    """Project management tool."""
    verbose: bool = argclass.Argument("-v", "--verbose", default=False,
                                      action=argclass.Actions.STORE_TRUE)
    init = InitCommand()
    build = BuildCommand()
    deploy = DeployCommand()

cli = CLI()
cli.parse_args(["build", "--output", "/tmp/out", "--release"])
assert cli.build.output == Path("/tmp/out")
assert cli.build.release is True

Config + Env + CLI¶

Real-world applications often need configuration from multiple sources: config files for deployment defaults, environment variables for container orchestration, and CLI arguments for ad-hoc overrides. argclass handles all three with a clear priority order: CLI > environment > config file.

This example demonstrates the complete configuration stack. It shows how defaults in a config file can be overridden by environment variables, which can in turn be overridden by command-line arguments. It also demonstrates secret handling with argclass.Secret and the sanitize_env() method.

Key features demonstrated:

  • Config file loading with config_files parameter

  • Automatic environment variable binding with auto_env_var_prefix

  • Priority order: CLI arguments override env vars override config

  • Secret masking with argclass.Secret

  • sanitize_env() to remove secrets from environment

  • Argument groups for organizing related settings

import os
import argclass
from pathlib import Path
from tempfile import NamedTemporaryFile

class DatabaseGroup(argclass.Group):
    """Database connection settings."""
    host: str = "localhost"
    port: int = 5432
    name: str = "app"
    password: str = argclass.Secret(env_var="DB_PASSWORD")

class ServerGroup(argclass.Group):
    """HTTP server settings."""
    host: str = "127.0.0.1"
    port: int = 8080
    workers: int = 4

class App(argclass.Parser):
    """Application server."""
    debug: bool = False
    log_level: str = argclass.Argument(
        default="info",
        choices=["debug", "info", "warning", "error"]
    )
    database = DatabaseGroup()
    server = ServerGroup()

# Config file (lowest priority)
CONFIG = """
[DEFAULT]
log_level = warning

[database]
host = db.example.com
port = 5432
name = production

[server]
host = 0.0.0.0
port = 80
workers = 8
"""

with NamedTemporaryFile(mode="w", suffix=".ini", delete=False) as f:
    f.write(CONFIG)
    config_path = f.name

# Environment variables (override config)
os.environ["APP_DEBUG"] = "true"
os.environ["APP_SERVER_PORT"] = "9000"
os.environ["DB_PASSWORD"] = "secret123"

app = App(
    config_files=[config_path],
    auto_env_var_prefix="APP_"
)

# CLI arguments (override everything)
app.parse_args(["--log-level", "debug"])

# Sanitize secrets
app.sanitize_env()

# Results
assert app.debug is True                    # From env
assert app.log_level == "debug"             # From CLI
assert app.database.host == "db.example.com"  # From config
assert app.database.port == 5432            # From config
assert str(app.database.password) == "secret123"  # From env
assert app.server.host == "0.0.0.0"         # From config
assert app.server.port == 9000              # From env (overrides config)
assert app.server.workers == 8              # From config

# Cleanup (use pop since sanitize_env may have removed some)
os.environ.pop("APP_DEBUG", None)
os.environ.pop("APP_SERVER_PORT", None)
Path(config_path).unlink()

Configuration Formats¶

argclass supports multiple configuration file formats out of the box. Each format can be used to provide default values that are overridden by environment variables and CLI arguments. Here are examples for each supported format.

INI Configuration¶

INI is the simplest format, ideal for flat configurations. Sections map to argument groups. Boolean values support various formats: true/false, yes/no, on/off, 1/0.

import argclass
from pathlib import Path
from tempfile import NamedTemporaryFile

class DatabaseGroup(argclass.Group):
    host: str = "localhost"
    port: int = 5432
    name: str = "mydb"

class Parser(argclass.Parser):
    debug: bool = False
    workers: int = 4
    database = DatabaseGroup()

CONFIG_INI = """
[DEFAULT]
debug = yes
workers = 8

[database]
host = db.example.com
port = 5432
name = production
"""

with NamedTemporaryFile(mode="w", suffix=".ini", delete=False) as f:
    f.write(CONFIG_INI)
    config_path = f.name

parser = Parser(config_files=[config_path])
parser.parse_args([])

assert parser.debug is True
assert parser.workers == 8
assert parser.database.host == "db.example.com"
assert parser.database.name == "production"

Path(config_path).unlink()

JSON Configuration¶

JSON is useful when you need structured data or when your config is generated programmatically. Nested objects map to argument groups.

import argclass
from pathlib import Path
from tempfile import NamedTemporaryFile

class ServerGroup(argclass.Group):
    host: str = "127.0.0.1"
    port: int = 8080

class Parser(argclass.Parser):
    debug: bool = False
    log_level: str = "info"
    server = ServerGroup()

CONFIG_JSON = """
{
    "debug": true,
    "log_level": "debug",
    "server": {
        "host": "0.0.0.0",
        "port": 9000
    }
}
"""

with NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
    f.write(CONFIG_JSON)
    config_path = f.name

parser = Parser(
    config_files=[config_path],
    config_parser_class=argclass.JSONDefaultsParser,
)
parser.parse_args([])

assert parser.debug is True
assert parser.log_level == "debug"
assert parser.server.host == "0.0.0.0"
assert parser.server.port == 9000

Path(config_path).unlink()

TOML Configuration¶

TOML provides a clean syntax popular in modern Python projects (like pyproject.toml). It has native support for different data types.

Note

TOML requires Python 3.11+ (stdlib tomllib) or the tomli package for Python 3.10.

import argclass
from pathlib import Path
from tempfile import NamedTemporaryFile

class CacheGroup(argclass.Group):
    enabled: bool = True
    ttl: int = 300
    backend: str = "memory"

class Parser(argclass.Parser):
    name: str = "myapp"
    version: str = "1.0.0"
    cache = CacheGroup()

CONFIG_TOML = """
name = "production-app"
version = "2.1.0"

[cache]
enabled = true
ttl = 3600
backend = "redis"
"""

with NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
    f.write(CONFIG_TOML)
    config_path = f.name

parser = Parser(
    config_files=[config_path],
    config_parser_class=argclass.TOMLDefaultsParser,
)
parser.parse_args([])

assert parser.name == "production-app"
assert parser.version == "2.1.0"
assert parser.cache.enabled is True
assert parser.cache.ttl == 3600
assert parser.cache.backend == "redis"

Path(config_path).unlink()

Multiple Config Files with Fallback¶

You can specify multiple config files. argclass reads them in order, with later files overriding earlier ones. Missing files are silently ignored, making this perfect for layered configuration.

import argclass
from pathlib import Path
from tempfile import NamedTemporaryFile

class Parser(argclass.Parser):
    host: str = "localhost"
    port: int = 8080
    debug: bool = False

# System-wide defaults
SYSTEM_CONFIG = """
[DEFAULT]
host = 0.0.0.0
port = 80
"""

# User overrides
USER_CONFIG = """
[DEFAULT]
port = 8080
debug = true
"""

with NamedTemporaryFile(mode="w", suffix=".ini", delete=False) as f:
    f.write(SYSTEM_CONFIG)
    system_path = f.name

with NamedTemporaryFile(mode="w", suffix=".ini", delete=False) as f:
    f.write(USER_CONFIG)
    user_path = f.name

parser = Parser(config_files=[
    "/etc/myapp/config.ini",  # Missing - ignored
    system_path,               # Provides host=0.0.0.0, port=80
    user_path,                 # Overrides port=8080, adds debug=true
])
parser.parse_args([])

assert parser.host == "0.0.0.0"   # From system config
assert parser.port == 8080        # Overridden by user config
assert parser.debug is True       # From user config

Path(system_path).unlink()
Path(user_path).unlink()

File Processing Tool¶

File processing utilities are one of the most common CLI applications. This example shows how to build a tool that accepts multiple input files, an output directory, and various processing options.

The pattern demonstrated here is useful for any batch processing tool: image converters, log analyzers, code formatters, or data transformers. The --dry-run flag is a best practice that lets users preview changes before committing to them.

Key features demonstrated:

  • Multiple input files with nargs="+" and list[Path]

  • Path type for automatic path handling

  • --dry-run pattern for safe previews

  • Boolean flags with explicit action=STORE_TRUE

  • Combining short (-n) and long (--dry-run) aliases

import argclass
from pathlib import Path

class FileProcessor(argclass.Parser):
    """Process files in batch."""
    input: list[Path] = argclass.Argument(
        "-i", "--input",
        nargs="+",
        help="Input files to process"
    )
    output: Path = argclass.Argument(
        "-o", "--output",
        default=Path("output"),
        help="Output directory"
    )
    pattern: str = argclass.Argument(
        "-p", "--pattern",
        default="*",
        help="Glob pattern to match"
    )
    recursive: bool = False
    dry_run: bool = argclass.Argument(
        "-n", "--dry-run",
        default=False,
        action=argclass.Actions.STORE_TRUE,
        help="Show what would be done"
    )

    def __call__(self) -> int:
        for path in self.input:
            if self.dry_run:
                print(f"Would process: {path}")
            else:
                print(f"Processing: {path}")
        return 0

processor = FileProcessor()
processor.parse_args([
    "-i", "file1.txt", "file2.txt",
    "-o", "/tmp/out",
    "--recursive",
    "--dry-run"
])

assert processor.input == [Path("file1.txt"), Path("file2.txt")]
assert processor.output == Path("/tmp/out")
assert processor.recursive is True
assert processor.dry_run is True

HTTP Client CLI¶

API clients often need flexible authentication and request configuration. This example shows how to build a curl-like tool with support for different HTTP methods, custom headers, request bodies, and authentication options.

The argument groups (AuthGroup, RequestOptions) keep related settings organized and make the --help output more readable. Using argclass.Secret for tokens and passwords ensures they won’t appear in logs or process listings.

Key features demonstrated:

  • Curl-like interface with -X, -H, -d options

  • Multiple headers with nargs="*" and list[str]

  • Optional arguments with str | None type

  • Secrets for sensitive authentication data

  • Grouped settings for better organization

  • choices for HTTP methods

import os
import argclass

class AuthGroup(argclass.Group):
    """Authentication settings."""
    token: str = argclass.Secret(env_var="API_TOKEN")
    username: str | None = None
    password: str = argclass.Secret()

class RequestOptions(argclass.Group):
    """Request configuration."""
    timeout: int = 30
    retries: int = 3
    verify_ssl: bool = True

class HTTPClient(argclass.Parser):
    """HTTP API client."""
    base_url: str = argclass.Argument(help="API base URL")
    method: str = argclass.Argument(
        "-X", "--method",
        default="GET",
        choices=["GET", "POST", "PUT", "DELETE", "PATCH"]
    )
    headers: list[str] = argclass.Argument(
        "-H", "--header",
        nargs="*",
        default=[],
        help="Headers in 'Key: Value' format"
    )
    data: str | None = argclass.Argument(
        "-d", "--data",
        default=None,
        help="Request body"
    )
    auth = AuthGroup()
    options = RequestOptions()

os.environ["API_TOKEN"] = "secret_token"

client = HTTPClient()
client.parse_args([
    "--base-url", "https://api.example.com",
    "-X", "POST",
    "-H", "Content-Type: application/json",
    "-d", '{"key": "value"}',
    "--options-timeout", "60"
])
client.sanitize_env()

assert client.base_url == "https://api.example.com"
assert client.method == "POST"
assert client.headers == ["Content-Type: application/json"]
assert client.data == '{"key": "value"}'
assert str(client.auth.token) == "secret_token"
assert client.options.timeout == 60

Database Migration Tool¶

Database migration tools like Alembic, Flyway, or Django migrations use subcommands for different operations: applying migrations, rolling back, checking status. This example shows how to structure such a tool with argclass.

The parent parser holds connection settings that apply to all subcommands, while each subcommand has its own specific options. This pattern is ideal for any tool that performs multiple related operations on a shared resource.

Key features demonstrated:

  • Multiple subcommands (up, down, status)

  • Shared parent options (--database, --migrations)

  • Environment variable fallback with env_var parameter

  • Optional target specification with str | None

  • --fake flag for marking migrations without running them

import argclass
from pathlib import Path

class MigrateUp(argclass.Parser):
    """Apply pending migrations."""
    target: str | None = argclass.Argument(
        default=None,
        help="Target migration (default: latest)"
    )
    fake: bool = argclass.Argument(
        default=False,
        action=argclass.Actions.STORE_TRUE,
        help="Mark as applied without running"
    )

    def __call__(self) -> int:
        target = self.target or "latest"
        print(f"Migrating up to {target}")
        return 0

class MigrateDown(argclass.Parser):
    """Rollback migrations."""
    steps: int = argclass.Argument(
        default=1,
        help="Number of migrations to rollback"
    )

    def __call__(self) -> int:
        print(f"Rolling back {self.steps} migration(s)")
        return 0

class MigrateStatus(argclass.Parser):
    """Show migration status."""
    def __call__(self) -> int:
        print("Showing migration status")
        return 0

class MigrateCLI(argclass.Parser):
    """Database migration tool."""
    database_url: str = argclass.Argument(
        "-d", "--database",
        env_var="DATABASE_URL",
        default="sqlite:///app.db"
    )
    migrations_dir: Path = argclass.Argument(
        "-m", "--migrations",
        default=Path("migrations")
    )
    up = MigrateUp()
    down = MigrateDown()
    status = MigrateStatus()

cli = MigrateCLI()
cli.parse_args(["--database", "postgres://localhost/app", "up", "--target", "002"])

assert cli.database_url == "postgres://localhost/app"
assert cli.up.target == "002"

Daemon/Service Configuration¶

Long-running services like web servers, message brokers, or background workers need extensive configuration: logging levels, metrics endpoints, process management options. This example shows how to organize these settings into logical groups.

The grouped approach (LoggingGroup, MetricsGroup) makes configuration manageable and the resulting --help output organized by category. This pattern works well for any application with many configuration options.

Key features demonstrated:

  • Complex configuration with multiple groups

  • Daemon-style options (--daemonize, --pid-file, --user)

  • Logging configuration with level and format choices

  • Metrics/monitoring endpoint configuration

  • Path | None for optional file paths

  • Short flags for common operations (-D, -c)

import argclass
from pathlib import Path

class LoggingGroup(argclass.Group):
    """Logging configuration."""
    level: str = argclass.Argument(
        default="info",
        choices=["debug", "info", "warning", "error"]
    )
    format: str = argclass.Argument(
        default="text",
        choices=["text", "json"]
    )
    file: Path | None = None

class MetricsGroup(argclass.Group):
    """Metrics and monitoring."""
    enabled: bool = True
    port: int = 9090
    path: str = "/metrics"

class Daemon(argclass.Parser):
    """Background service daemon."""
    config: Path | None = argclass.Argument(
        "-c", "--config",
        default=None,
        help="Config file path"
    )
    pid_file: Path = argclass.Argument(
        default=Path("/var/run/myapp.pid")
    )
    daemonize: bool = argclass.Argument(
        "-D", "--daemonize",
        default=False,
        action=argclass.Actions.STORE_TRUE
    )
    user: str | None = None
    group: str | None = None
    logging = LoggingGroup()
    metrics = MetricsGroup()

daemon = Daemon()
daemon.parse_args([
    "--daemonize",
    "--logging-level", "debug",
    "--logging-file", "/var/log/myapp.log",
    "--metrics-port", "8080"
])

assert daemon.daemonize is True
assert daemon.logging.level == "debug"
assert daemon.logging.file == Path("/var/log/myapp.log")
assert daemon.metrics.port == 8080

Testing Your CLI¶

Testing CLI parsers is straightforward because argclass parsers are just Python classes. You can instantiate them, call parse_args() with test arguments, and assert on the resulting attribute values.

The examples below show common testing patterns using pytest. Each pattern addresses a specific testing need: basic argument parsing, environment variable handling, config file loading, and subcommand dispatch.

Basic Test¶

The simplest test pattern: create a parser instance, parse known arguments, and verify the results. This works for any parser and catches regressions in argument definitions.

import pytest
import argclass

class Parser(argclass.Parser):
    name: str
    count: int = 1

def test_parser_defaults():
    parser = Parser()
    parser.parse_args(["--name", "test"])
    assert parser.name == "test"
    assert parser.count == 1

def test_parser_all_args():
    parser = Parser()
    parser.parse_args(["--name", "test", "--count", "5"])
    assert parser.name == "test"
    assert parser.count == 5

Testing with Environment¶

Use pytest’s monkeypatch fixture to set environment variables for tests. This isolates each test and ensures environment changes don’t leak between tests.

def test_with_env(monkeypatch):
    monkeypatch.setenv("APP_HOST", "test-host")

    class Parser(argclass.Parser):
        host: str = argclass.Argument(env_var="APP_HOST", default="localhost")

    parser = Parser()
    parser.parse_args([])
    assert parser.host == "test-host"

Testing with Config Files¶

Use pytest’s tmp_path fixture to create temporary config files. This ensures tests are isolated and don’t depend on files in your filesystem.

def test_with_config(tmp_path):
    config_file = tmp_path / "config.ini"
    config_file.write_text("[DEFAULT]\nport = 9000\n")

    class Parser(argclass.Parser):
        port: int = 8080

    parser = Parser(config_files=[str(config_file)])
    parser.parse_args([])
    assert parser.port == 9000

Testing Subcommands¶

Test subcommand dispatch by parsing arguments that include the subcommand name, then call the parser to execute the selected subcommand.

def test_subcommand():
    class Sub(argclass.Parser):
        value: int = 1
        def __call__(self): return self.value

    class CLI(argclass.Parser):
        sub = Sub()

    cli = CLI()
    cli.parse_args(["sub", "--value", "42"])
    assert cli() == 42
Next
Arguments
Previous
Tutorial
Copyright © 2026, Dmitry Orlov
Made with Sphinx and @pradyunsg's Furo
On this page
  • Examples Gallery
    • Simple CLI Tool
    • Subcommand CLI (git-style)
    • Config + Env + CLI
    • Configuration Formats
      • INI Configuration
      • JSON Configuration
      • TOML Configuration
      • Multiple Config Files with Fallback
    • File Processing Tool
    • HTTP Client CLI
    • Database Migration Tool
    • Daemon/Service Configuration
    • Testing Your CLI
      • Basic Test
      • Testing with Environment
      • Testing with Config Files
      • Testing Subcommands