Common Pitfalls¶
Quick reference for common mistakes and their solutions. These are the issues most frequently encountered when building CLI applications with argclass.
Boolean Flags¶
Boolean arguments are the most common source of confusion. The shortcut syntax
bool = False automatically creates a flag, but using Argument() requires
explicit action configuration.
Syntax |
Behavior |
|---|---|
|
|
|
|
|
Expects value like |
|
Works as flag |
Rule: Use simple bool = False syntax. Only use Argument() for booleans
when you need help text or aliases, and always include action=Actions.STORE_TRUE.
Warning
A common mistake is bool = True expecting --flag to enable a feature.
Instead, --flag will disable it (set to False). If you want a feature
enabled by default that users can disable, name it --no-feature with bool = True.
import argclass
class Parser(argclass.Parser):
feature: bool = True # --feature toggles to False
parser = Parser()
parser.parse_args(["--feature"])
assert parser.feature is False
Environment Variables¶
Environment variables are strings, so argclass must parse them into the appropriate types. Boolean parsing is particularly tricky because there’s no universal standard for representing true/false in environment variables.
Issue |
Solution |
|---|---|
Boolean strings |
See table below (case-insensitive) |
Spaces preserved |
Trim in application logic: |
Type errors |
Same rules as CLI — invalid values exit with code 2 |
Boolean String Parsing¶
argclass recognizes common conventions for boolean environment variables. Values are case-insensitive.
Parsed as |
Parsed as |
|---|---|
|
Everything else |
|
|
import os
import argclass
os.environ["TEST_FLAG"] = "yes" # Also: true, 1, on, enable
class Parser(argclass.Parser):
flag: bool = argclass.Argument(env_var="TEST_FLAG", default=False)
parser = Parser()
parser.parse_args([])
assert parser.flag is True
del os.environ["TEST_FLAG"]
import os
import argclass
os.environ["TEST_FLAG"] = "no" # Also: false, 0, off, disable, or any other string
class Parser(argclass.Parser):
flag: bool = argclass.Argument(env_var="TEST_FLAG", default=False)
parser = Parser()
parser.parse_args([])
assert parser.flag is False
os.environ.pop("TEST_FLAG", None)
Lists¶
List arguments have subtle behavior differences depending on nargs configuration.
The most common mistake is using nargs="+" when you want to allow empty lists.
Issue |
Solution |
|---|---|
|
Use |
Comma-separated values |
CLI uses spaces: |
Default |
Requires at least one value when flag is used |
Tip
Use nargs="*" if the flag can appear with zero values (--files alone is valid).
Use nargs="+" if at least one value is required when the flag is used.
import argclass
class Parser(argclass.Parser):
files: list[str] = argclass.Argument(nargs="*", default=[])
parser = Parser()
parser.parse_args(["--files"]) # Zero values OK with nargs="*"
assert parser.files == []
Type Hints¶
Type hints determine whether arguments are required or optional. A common
surprise is that T | None without a default value implies default=None,
making the argument optional rather than required.
Hint |
Behavior |
|---|---|
|
Required argument |
|
Optional with default |
|
Optional, defaults to |
|
Auto-converts string to |
Note
The | None union type automatically sets default=None. If you want a
required argument that can accept None as a valid CLI value, you’ll need
custom handling.
import argclass
class Parser(argclass.Parser):
config: str | None # Implies default=None, NOT required
parser = Parser()
parser.parse_args([])
assert parser.config is None
Config Files (INI)¶
INI config files have specific formatting requirements. Section names must exactly match group attribute names (case-sensitive), and complex types like lists use Python literal syntax, not comma-separated values.
Issue |
Solution |
|---|---|
Section name mismatch |
Section must match group attribute name (lowercase) |
Lists as comma-separated |
Use Python literal: |
Strings in lists |
Quote them: |
# Group attribute: database = DatabaseGroup()
[database] # RIGHT - matches attribute name
host = db.example.com
[Database] # WRONG - case mismatch (won't be loaded)
Warning
INI section names are case-sensitive in argclass. [Database] and [database]
are different sections. Always use lowercase to match Python attribute names.
Groups¶
When you add a group to a parser, all its arguments get prefixed with the group’s attribute name. This is a common source of confusion when users expect unprefixed argument names.
class Parser(argclass.Parser):
database = DatabaseGroup() # prefix is "database"
# CLI usage:
--database-host value # RIGHT
--host value # WRONG - no such argument
Tip
To add group arguments without a prefix, use prefix="":
import argclass
class DatabaseGroup(argclass.Group):
host: str = "localhost"
port: int = 5432
class Parser(argclass.Parser):
database = DatabaseGroup(prefix="") # Arguments: --host, --port
Subcommands¶
When using subcommands, only the selected subcommand’s arguments are parsed and populated. Other subcommands retain their default values. Don’t assume all subcommand attributes are populated after parsing.
import argclass
class Serve(argclass.Parser):
port: int = 8080
class Build(argclass.Parser):
output: str = "dist"
class CLI(argclass.Parser):
serve = Serve()
build = Build()
cli = CLI()
cli.parse_args(["serve", "--port", "9000"])
assert cli.serve.port == 9000
# cli.build.output is still default
Tip
Use cli.current_subparsers to check which subcommand was selected, or
implement __call__ on each subcommand and call cli() to dispatch
automatically to the selected command.
Exception-Raising Patterns¶
These patterns will raise specific argclass exceptions at parser definition or parsing time.
ComplexTypeError: Unsupported Union Types¶
Union types like str | int cannot be automatically converted because argclass
doesn’t know which type to try first. You must provide an explicit converter.
Pattern |
Result |
|---|---|
|
|
|
OK — |
|
OK — |
import argclass
# This works - Optional types are supported
class WorkingParser(argclass.Parser):
name: str | None # OK: Union with None
parser = WorkingParser()
parser.parse_args([])
assert parser.name is None
To fix union types, provide an explicit converter:
import argclass
def flexible_int(value: str) -> int | str:
try:
return int(value)
except ValueError:
return value
class Parser(argclass.Parser):
count: int | str = argclass.Argument(type=flexible_int, default=0)
EnumValueError: Invalid Enum Defaults¶
When using EnumArgument, the default must be a valid enum member or its
string name. Providing an invalid default raises EnumValueError.
import argclass
from enum import Enum
class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
# Correct: default is a valid enum member name
class Parser(argclass.Parser):
color: Color = argclass.EnumArgument(Color, default="RED")
parser = Parser()
parser.parse_args([])
assert parser.color == Color.RED
ArgumentDefinitionError: Conflicting Aliases¶
If you define an alias that conflicts with another argument or a reserved
argparse option, ArgumentDefinitionError is raised.
import argclass
# This works - no conflicts
class Parser(argclass.Parser):
verbose: bool = argclass.Argument("-v", default=False)
output: str = argclass.Argument("-o", default="out.txt")
parser = Parser()
parser.parse_args(["-v", "-o", "result.txt"])
assert parser.verbose is True
assert parser.output == "result.txt"
TypeConversionError: Converter Failures¶
When a custom converter raises an exception, argclass wraps it in
TypeConversionError with context about what value failed and the target type.
import argclass
def positive_int(value: str) -> int:
num = int(value)
if num <= 0:
raise ValueError(f"{value} must be positive")
return num
class Parser(argclass.Parser):
count: int = argclass.Argument(type=positive_int, default=1)
parser = Parser()
parser.parse_args(["--count", "5"])
assert parser.count == 5
ConfigurationError: Invalid Config Files¶
When loading config files with config_files parameter, malformed files or
type mismatches raise ConfigurationError.
Issue |
Result |
|---|---|
Malformed INI/JSON/TOML |
|
Value doesn’t match type |
|
Missing file |
Silently ignored (unless |
import argclass
from pathlib import Path
from tempfile import NamedTemporaryFile
class Parser(argclass.Parser):
host: str = "localhost"
port: int = 8080
# Create a valid config file
with NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
f.write('{"host": "example.com", "port": 9000}')
config_path = f.name
parser = Parser(
config_files=[config_path],
config_parser_class=argclass.JSONDefaultsParser,
)
parser.parse_args([])
assert parser.host == "example.com"
assert parser.port == 9000
Path(config_path).unlink()