Argument Groups¶
Groups organize related arguments together and enable reuse across parsers.
They provide logical structure to your CLI, make --help output more readable,
and allow you to define common argument sets once and reuse them in multiple parsers.
The Mental Model: a Group Instance Is a Declaration, not a Parser¶
A Group() you assign on a parser class — db = DatabaseGroup() or
db: DatabaseGroup = DatabaseGroup(defaults={...}) — is a declaration
of structure and per-instance defaults, not a runtime parser. You
don’t call methods on it; you don’t use it to read CLI arguments. Its
job at class-definition time is to:
name a slot in the parsed result (
parser.db),describe which arguments belong to that slot (via its annotations),
optionally override defaults for this particular slot (
defaults=,title=,prefix=).
When you call Parser().parse_args(...), argclass walks these
declarations, builds an argparse parser from them, parses the
command line, and then writes the parsed values back into the same
group instance so you can read them as parser.db.host. The instance
is a write target during parsing, not an active participant in it.
Two consequences worth internalising:
Don’t call parser methods on a
Groupinstance. It has noparse_args(). Groups are not standalone parsers.Don’t share one
Groupinstance across two attributes. Since the instance holds the parsed state for its slot, assigning the same instance toprimary = sharedandsecondary = sharedwould make them aliases of one another — argclass raisesArgclassErrorat parse time to prevent this. Construct oneGroup()per slot. (Using the same Group class twice is fine — only sharing a single constructed instance is not.)
Note
Groups vs. subparsers. This “instance-is-a-declaration” rule is
specific to Group. Subparsers are different: a subparser is a
Parser subclass instance assigned to an attribute (e.g.
serve = Serve()), and at runtime the selected subparser really does
parse its own slice of sys.argv — it has a working parse_args(),
its own __call__, its own subparsers. Subparsers are real parsers
chosen by name from the CLI; groups are namespaced collections of
arguments declared upfront. If you want a runnable sub-command, use a
subparser. If you want to bundle related options under a prefix, use a
group. See Subparsers for the runtime contract.
Basic Groups¶
Create a group by inheriting from argclass.Group. When you add a group to a
parser, its arguments are prefixed with the attribute name. Here, database
becomes the prefix, so arguments become --database-host, --database-port, etc.
import argclass
class DatabaseGroup(argclass.Group):
host: str = "localhost"
port: int = 5432
user: str = "admin"
class Parser(argclass.Parser):
verbose: bool = False
database = DatabaseGroup()
parser = Parser()
parser.parse_args(["--database-host", "db.example.com", "--database-port", "3306"])
assert parser.verbose is False
assert parser.database.host == "db.example.com"
assert parser.database.port == 3306
assert parser.database.user == "admin"
Group Titles¶
Add a descriptive title that appears in --help output. This makes the help
more readable by clearly labeling each section of related arguments.
import argclass
class DatabaseGroup(argclass.Group):
host: str = "localhost"
port: int = 5432
class Parser(argclass.Parser):
database = DatabaseGroup(title="Database connection")
parser = Parser()
parser.parse_args(["--database-host", "db.example.com"])
assert parser.database.host == "db.example.com"
assert parser.database.port == 5432
Custom Prefixes¶
Override the default prefix with prefix=. Use an empty string to add
arguments without any prefix. This is useful when you want short argument
names or when the group represents the main configuration.
import argclass
class ConnectionGroup(argclass.Group):
host: str = "localhost"
port: int = 8080
class Parser(argclass.Parser):
# Custom prefix: --api-host, --api-port
api = ConnectionGroup(prefix="api")
# No prefix: --host, --port
server = ConnectionGroup(prefix="")
parser = Parser()
parser.parse_args([
"--api-host", "api.example.com",
"--api-port", "9000",
"--host", "server.example.com",
"--port", "3000"
])
assert parser.api.host == "api.example.com"
assert parser.api.port == 9000
assert parser.server.host == "server.example.com"
assert parser.server.port == 3000
Reusing Groups¶
The same group class can be instantiated multiple times with different
settings. Use defaults= to override default values for each instance.
This avoids duplicating group definitions for similar configurations.
import argclass
class HostPort(argclass.Group):
host: str = "localhost"
port: int
class Parser(argclass.Parser):
api = HostPort(title="API Server", defaults={"port": 8080})
metrics = HostPort(title="Metrics Server", defaults={"port": 9090})
database = HostPort(title="Database", defaults={"port": 5432})
parser = Parser()
parser.parse_args([
"--api-host", "0.0.0.0",
"--metrics-port", "9999"
])
assert parser.api.host == "0.0.0.0"
assert parser.api.port == 8080
assert parser.metrics.host == "localhost"
assert parser.metrics.port == 9999
assert parser.database.port == 5432
Group Defaults¶
Use defaults= to provide instance-specific default values. This is useful
for deployment presets like production vs development configurations, where
the same group structure needs different default values.
import argclass
class ServerGroup(argclass.Group):
host: str = "localhost"
port: int = 8080
ssl: bool = False
class Parser(argclass.Parser):
prod = ServerGroup(defaults={
"host": "0.0.0.0",
"port": 443,
"ssl": True,
})
parser = Parser()
parser.parse_args([])
assert parser.prod.host == "0.0.0.0"
assert parser.prod.port == 443
assert parser.prod.ssl is True
Inheriting from Groups¶
Parsers can inherit from groups as mixins to include arguments directly at the top level (without a prefix). This is useful for common arguments like logging or verbosity that you want available in multiple parsers.
import argclass
class LoggingMixin(argclass.Group):
log_level: str = "info"
log_file: str | None = None
class VerboseMixin(argclass.Group):
verbose: bool = False
quiet: bool = False
class Parser(argclass.Parser, LoggingMixin, VerboseMixin):
name: str
parser = Parser()
parser.parse_args(["--name", "test", "--log-level", "debug", "--verbose"])
assert parser.name == "test"
assert parser.log_level == "debug"
assert parser.verbose is True
assert parser.quiet is False
Accessing Group Values¶
After parsing, access group values through the group attribute. Groups behave like regular Python objects - use dot notation to read the parsed values.
import argclass
class DatabaseGroup(argclass.Group):
host: str = "localhost"
port: int = 5432
class Parser(argclass.Parser):
database = DatabaseGroup()
parser = Parser()
parser.parse_args(["--database-host", "db.example.com"])
# Access via group
assert parser.database.host == "db.example.com"
assert parser.database.port == 5432
Nested Groups¶
Groups can contain other Groups as fields. This works for any depth and
keeps Parser → Group → Group → ... cleanly modelled in code. Names are
built by joining the attribute path with the appropriate separator for
each source:
Source |
Separator |
Example |
|---|---|---|
CLI |
|
|
ENV |
|
|
INI |
|
|
JSON |
nested |
|
TOML |
|
|
import argclass
class Credentials(argclass.Group):
username: str = "admin"
password: str = "secret"
class Endpoint(argclass.Group):
host: str = "localhost"
port: int = 8080
credentials: Credentials = Credentials()
class Parser(argclass.Parser):
endpoint: Endpoint = Endpoint()
parser = Parser()
parser.parse_args([
"--endpoint-host", "api.example.com",
"--endpoint-credentials-username", "root",
"--endpoint-credentials-password", "hunter2",
])
assert parser.endpoint.host == "api.example.com"
assert parser.endpoint.credentials.username == "root"
assert parser.endpoint.credentials.password == "hunter2"
Nested groups appear as separate sections in --help, titled with their
dotted attribute path (e.g. endpoint.credentials). Set title= on a
group to override the default title for that one level.
Nested groups in config files¶
INI sections use a dotted section name; JSON/TOML use natural nesting.
import argclass
from pathlib import Path
from tempfile import NamedTemporaryFile
class Credentials(argclass.Group):
username: str = "admin"
password: str = "secret"
class Endpoint(argclass.Group):
host: str = "localhost"
credentials: Credentials = Credentials()
class Parser(argclass.Parser):
endpoint: Endpoint = Endpoint()
CONFIG = """
[endpoint]
host = api.example.com
[endpoint.credentials]
username = root
password = hunter2
"""
with NamedTemporaryFile(mode="w", suffix=".ini", delete=False) as f:
f.write(CONFIG)
config_path = f.name
parser = Parser(config_files=[config_path])
parser.parse_args([])
assert parser.endpoint.host == "api.example.com"
assert parser.endpoint.credentials.username == "root"
assert parser.endpoint.credentials.password == "hunter2"
Path(config_path).unlink()
prefix= and nested groups¶
Group(prefix=...) overrides only the CLI/ENV segment for that group. It
does not affect the INI/TOML section name — config sections always
follow the attribute path. This keeps section names predictable and
prevents CLI prefixes from silently desyncing from config layout.
Group fields with type annotations¶
A group attribute can be declared with a type annotation. When the
annotation refers to a Group subclass, argclass enforces these rules
at class definition time:
Form |
Behaviour |
|---|---|
|
Auto-instantiated as |
|
Uses the provided instance |
|
Auto-instantiated as |
|
Uses the provided instance |
|
Rejected (Group can’t be None) |
|
Rejected (Group can’t be None) |
|
Rejected |
|
Rejected |
import argclass
class DatabaseGroup(argclass.Group):
host: str = "localhost"
port: int = 5432
class Parser(argclass.Parser):
# No explicit default — argclass instantiates DatabaseGroup() for you
database: DatabaseGroup
parser = Parser()
parser.parse_args(["--database-host", "db.example.com"])
assert parser.database.host == "db.example.com"
assert parser.database.port == 5432
Rejected forms raise ArgumentDefinitionError immediately when the
parser class is defined, with a hint suggesting the correct form.
Reusing a group instance is an error¶
Passing the same Group instance to two different attributes raises
ArgclassError, because that group’s parsed state would otherwise be
shared between locations. Instantiate a separate group per attribute,
or subclass Group to define a dedicated type:
import argclass
class Credentials(argclass.Group):
username: str = "admin"
class Auth(argclass.Group):
primary: Credentials = Credentials() # separate instance
secondary: Credentials = Credentials() # separate instance
class Parser(argclass.Parser):
auth: Auth = Auth()
parser = Parser()
parser.parse_args([
"--auth-primary-username", "alice",
"--auth-secondary-username", "bob",
])
assert parser.auth.primary.username == "alice"
assert parser.auth.secondary.username == "bob"
Groups in Config Files¶
Groups map to INI sections. The section name matches the group attribute name.
Top-level parser arguments go in [DEFAULT], while each group gets its own
section named after the attribute.
import argclass
from pathlib import Path
from tempfile import NamedTemporaryFile
class ConnectionGroup(argclass.Group):
host: str = "localhost"
port: int = 8080
class Parser(argclass.Parser):
verbose: bool = False
database = ConnectionGroup()
cache = ConnectionGroup()
CONFIG = """
[DEFAULT]
verbose = true
[database]
host = db.example.com
port = 5432
[cache]
host = redis.example.com
port = 6379
"""
with NamedTemporaryFile(mode="w", suffix=".ini", delete=False) as f:
f.write(CONFIG)
config_path = f.name
parser = Parser(config_files=[config_path])
parser.parse_args([])
assert parser.verbose is True
assert parser.database.host == "db.example.com"
assert parser.database.port == 5432
assert parser.cache.host == "redis.example.com"
assert parser.cache.port == 6379
Path(config_path).unlink()