Declarative CLI parser for Python over argparse. Type hints → CLI args automatically. str→string, bool→flag, Optional[T]→optional, list[T]→multi-value, Literal[...]→choices. Priority: defaults < config files < env vars < CLI args. Zero deps, stdlib only, Python 3.10-3.14.
uv sync # install deps
uv run pytest -vv --cov=argclass --cov-report=term-missing --doctest-modules tests # full test suite
uv run pytest tests/test_simple.py::TestClassName::test_method -vv # single test
uv run ruff check # lint
uv run ruff format --check # verify formatting
uv run ruff format # apply formatting
uv run mypy # type checkingimport argclass
class CLI(argclass.Parser):
host: str = "localhost" # --host (default: localhost)
port: int = 8080 # --port (default: 8080)
verbose: bool = False # --verbose flag (store_true)
parser = CLI()
parser.parse_args()from typing import Optional, Literal
import argclass
class CLI(argclass.Parser):
name: Optional[str] = None # --name (optional)
tags: list[str] = argclass.Argument( # --tags a b c
nargs=argclass.Nargs.ONE_OR_MORE,
)
mode: Literal["fast", "slow"] = "fast" # --mode {fast,slow}import argclass
class Database(argclass.Group):
host: str = "localhost"
port: int = 5432
class CLI(argclass.Parser):
debug: bool = False
db: Database = Database(title="Database options")
parser = CLI()
parser.parse_args()
print(parser.db.host, parser.db.port)Groups can contain other Groups for arbitrary nesting:
import argclass
class Credentials(argclass.Group):
username: str = "admin"
password: str = "secret"
class Endpoint(argclass.Group):
host: str = "localhost"
credentials: Credentials = Credentials()
class CLI(argclass.Parser):
endpoint: Endpoint = Endpoint()
parser = CLI()
parser.parse_args([
"--endpoint-host=10.0.0.1",
"--endpoint-credentials-username=root",
])Naming follows the attribute path:
- CLI:
--endpoint-credentials-username - ENV (with
auto_env_var_prefix="APP_"):APP_ENDPOINT_CREDENTIALS_USERNAME - INI: section
[endpoint.credentials], keyusername - JSON/TOML: nested objects/tables
Group(prefix=...) overrides only the CLI/env segment for that group;
config section names always follow the attribute path. Reusing the
same Group instance in two places raises ArgclassError (instantiate
a separate Group per attribute).
import argclass
class Serve(argclass.Parser):
port: int = 8080
class Deploy(argclass.Parser):
target: str = "production"
class CLI(argclass.Parser):
serve = Serve()
deploy = Deploy()import argclass
class CLI(argclass.Parser):
db_url: str = argclass.Argument(env_var="DATABASE_URL")
api_key: str = argclass.Secret() # masked in repr/logs
# auto_env_var_prefix generates env vars from arg names
parser = CLI(auto_env_var_prefix="APP_")
parser.parse_args()
parser.sanitize_env(only_secrets=True)class CLI(argclass.Parser):
debug: bool = False
db: Database = Database(title="Database options")
parser = CLI()
parser.parse_args(["--debug", "--db-host=127.0.0.1", "--db-port", "9876"])
assert parser.debug is True
assert parser.db.host == "127.0.0.1"Metaclass-driven: Meta metaclass in parser.py processes annotations at class definition time, creating __arguments__, __argument_groups__, __subparsers__.
parser.py—Meta,Base,Parser,Group,Destination. Argparse integration, config files, env vars.factory.py—Argument,ArgumentSingle,ArgumentSequence,EnumArgument,Secret,Config,LogLevelfactories.store.py—StoreMeta/Storefor typed field storage;ArgumentBase/TypedArgumentextend it.defaults.py— Config file parsers (INI/JSON/TOML) implementingAbstractDefaultsParser.actions.py— Custom argparseActionsubclasses.secret.py—SecretStringmasks values in repr/logging.exceptions.py—ArgclassErrorhierarchy with suggestions.types.py— Type aliases, enums (Actions,Nargs,LogLevelEnum), constants.utils.py—parse_bool,read_ini_configs, type introspection helpers, annotation merging.
- Line length: 80 chars (ruff). Rules: E, F, W, C90 (C901 ignored)
- mypy strict for
argclass/, relaxed fortests/ - Build backend: hatchling