A small Python CLI library that provides Pydantic-based argument parser with type safety.
For detailed specifications, see CLAUDE.md.
- Type-safe CLI arguments with Pydantic validation
- Decorator-based design for clean, readable code
- Automatic argument parsing from class definitions
- Enum support — string and integer valued enums, parsed by name or value
- Positional arguments — with optional defaults
- List/array arguments —
List[str],List[int],List[Enum] - Environment variable fallback — CLI > ENV > default priority
- Subcommands — git-style CLI with
@kliamka_subcommands - Short flags —
-valongside--verbose - Mutually exclusive groups — e.g.
--jsonvs--csv --versionflag — automatic version display- Help customization — program name, usage, epilog
- Custom type converters — per-argument
converter=or a globalregister_converter(type, fn)registry - Pydantic validators — range checks, cross-field validation, regex patterns
- Programmatic argv — pass custom argument lists for testing/embedding
- PEP 561 compatible — ships
py.typedmarker - Modern Python 3.11+ with full type hints
pip install kliamkafrom kliamka import KliamkaArg, KliamkaArgClass, kliamka_cli
class MyArgs(KliamkaArgClass):
"""My CLI application arguments."""
verbose: bool | None = KliamkaArg("--verbose", "Enable verbose output", short="-v")
count: int | None = KliamkaArg("--count", "Number of iterations", default=1, short="-c")
@kliamka_cli(MyArgs)
def main(args: MyArgs) -> None:
if args.verbose:
print("Verbose mode enabled")
for i in range(args.count or 1):
print(f"Iteration {i + 1}")
if __name__ == "__main__":
main()python my_app.py -v -c 3Descriptor for defining CLI arguments.
KliamkaArg(
flag: str, # Flag name: "--verbose" or "filename"
help_text: str = "", # Help text
default: Any = None, # Default value
positional: bool = False, # Positional argument
env: str | None = None, # Environment variable fallback
short: str | None = None, # Short flag: "-v"
mutually_exclusive: str | None = None, # Exclusion group name
)Base class for CLI argument definitions using Pydantic models.
class MyArgs(KliamkaArgClass):
"""Description shown in --help."""
debug: bool | None = KliamkaArg("--debug", "Enable debug mode", short="-d")
config: str | None = KliamkaArg("--config", "Config file path")Customize help output and add --version:
from kliamka import ParserMeta
class MyArgs(KliamkaArgClass):
"""My application."""
parser_meta = ParserMeta(
prog="myapp",
usage="myapp [options] FILE",
epilog="See https://example.com for docs.",
version="myapp 1.0.0",
)
verbose: bool | None = KliamkaArg("--verbose", "Verbose", short="-v")Decorator that parses CLI arguments and injects them as the first parameter.
@kliamka_cli(MyArgs)
def main(args: MyArgs) -> None:
pass
# Or with custom argv for testing:
@kliamka_cli(MyArgs, argv=["--verbose", "--count", "5"])
def main(args: MyArgs) -> None:
passDecorator for git-style subcommand CLIs.
from kliamka import kliamka_subcommands
class MainArgs(KliamkaArgClass):
verbose: bool | None = KliamkaArg("--verbose", "Verbose output", short="-v")
class AddArgs(KliamkaArgClass):
"""Add a new item."""
name: str = KliamkaArg("name", "Item name", positional=True)
class RemoveArgs(KliamkaArgClass):
"""Remove an item."""
id: int = KliamkaArg("id", "Item ID", positional=True)
force: bool | None = KliamkaArg("--force", "Force removal", short="-f")
@kliamka_subcommands(MainArgs, {"add": AddArgs, "remove": RemoveArgs})
def main(args: MainArgs, command: str, cmd_args) -> None:
if command == "add":
print(f"Adding {cmd_args.name}")
elif command == "remove":
print(f"Removing {cmd_args.id} (force={cmd_args.force})")from enum import Enum
class LogLevel(Enum):
DEBUG = "debug"
INFO = "info"
ERROR = "error"
class MyArgs(KliamkaArgClass):
level: LogLevel = KliamkaArg("--level", "Log level", LogLevel.INFO)Accepts --level debug, --level DEBUG, or --level info.
class MyArgs(KliamkaArgClass):
api_key: str | None = KliamkaArg("--api-key", "API key", env="MY_API_KEY")
debug: bool | None = KliamkaArg("--debug", "Debug mode", env="DEBUG")Priority: CLI argument > environment variable > default value.
from typing import List
class MyArgs(KliamkaArgClass):
files: List[str] = KliamkaArg("--files", "Input files")
counts: List[int] = KliamkaArg("--counts", "Counts")python app.py --files a.txt b.txt --counts 1 2 3class MyArgs(KliamkaArgClass):
source: str = KliamkaArg("source", "Source file", positional=True)
dest: str = KliamkaArg("dest", "Destination file", positional=True)class MyArgs(KliamkaArgClass):
json_out: bool | None = KliamkaArg(
"--json", "JSON output", mutually_exclusive="format"
)
csv_out: bool | None = KliamkaArg(
"--csv", "CSV output", mutually_exclusive="format"
)--json and --csv cannot be used together.
kliamka has built-in support for bool, int, float, str, Enum, and List[...]. For anything else — pathlib.Path, datetime, uuid.UUID, custom classes — you can plug in a converter.
Two ways, first match wins:
1. Per-argument converter (explicit, scoped):
from pathlib import Path
from kliamka import KliamkaArg, KliamkaArgClass
class MyArgs(KliamkaArgClass):
config: Path = KliamkaArg(
"--config",
"Path to config file",
converter=lambda s: Path(s).expanduser().resolve(),
)2. Global registry (reusable across all arguments of a given type):
from datetime import datetime
from kliamka import KliamkaArgClass, KliamkaArg, register_converter
register_converter(datetime, datetime.fromisoformat)
class MyArgs(KliamkaArgClass):
since: datetime = KliamkaArg("--since", "Start timestamp")
until: datetime = KliamkaArg("--until", "End timestamp")Resolution order (first match wins, used by both CLI and env var parsing):
- Explicit
KliamkaArg(converter=...)on the field - A type registered via
register_converter(...) Enumsubclass — built-in enum parserList[T]— recursive lookup on element typeT- Fallback: the annotation itself (e.g.
int,float,str)
Converters can raise ValueError or TypeError on invalid input; kliamka wraps that into a clean CLI error like:
error: argument --port: invalid int value: '99999' (out of range: 99999)
Use unregister_converter(type) to remove a registered converter. See examples/custom_converters.py for a runnable demo.
from pydantic import model_validator
class MyArgs(KliamkaArgClass):
port: int | None = KliamkaArg("--port", "Port number", default=8080)
@model_validator(mode="after")
def validate_port(self) -> "MyArgs":
if self.port is not None and not (1 <= self.port <= 65535):
raise ValueError(f"Port must be 1-65535, got {self.port}")
return selfWhen validation fails during from_args(), kliamka raises KliamkaError with a simplified message. When validation fails inside @kliamka_cli or @kliamka_subcommands, the library renders that message through argparse, so users see standard CLI output such as:
error: Port must be 1-65535, got 99999
CLAUDE.md— source of truth for project specificationsdocs/TODO.md— development log and task trackerdocs/20260408_product_requirements.md— current product requirements and roadmap baseline
- Python 3.11+
- Pydantic 2.0+
git clone https://github.com/hotsyk/kliamka.git
cd kliamka
make init-dev
make install| Command | Description |
|---|---|
make install |
Install package in development mode |
make test |
Run unit tests with pytest (-m "not packaging") |
make test-package |
Build wheel/sdist and run packaging smoke tests |
make test-all |
Run the full pytest suite, including packaging smoke coverage |
make test-docker VERSION=3.11 |
Run tests in Docker for a specific Python version |
make test-docker-all |
Run Docker test matrix for Python 3.11–3.14 |
make lint |
Run type checking and linting |
make format |
Format code with ruff |
make clean |
Clean build artifacts |
The repository now uses a modular package layout internally. The public import surface remains from kliamka import ..., while the active implementation lives under src/kliamka/ and the legacy top-level module path is kept as a compatibility facade.
Validation failures are also split more cleanly by context:
KliamkaArgClass.from_args()raisesKliamkaErrorwith simplified, joined validation messages for programmatic use.@kliamka_cliand@kliamka_subcommandshand those messages back toargparse, so CLI users still see standarderror:output.
GitHub Actions verifies both compatibility and packaging confidence on Python 3.11, 3.12, 3.13, and 3.14. The default local workflow mirrors that split:
make testfor unit coverage excluding packaging smoke markersmake test-packagefor built wheel/sdist installation smoke testsmake test-allfor the complete pytest suite
To mirror the pytest compatibility check locally across interpreter versions, use Dockerfile.test.
Run the full local Docker matrix:
make test-docker-allThis target runs four isolated Docker test jobs sequentially for Python 3.11, 3.12, 3.13, and 3.14.
Run one Dockerized test job for a specific Python version:
make test-docker VERSION=3.12An optional docker-compose.test.yml file is included as a simple service definition reference for the supported Python versions.
These Docker targets bootstrap their own isolated environment inside the container and do not rely on the host .venv or on make test. Each container installs the package and runs:
pip install -e .
pip install pytest
pytest tests/ -v --tb=shortSuccess means the selected container exits cleanly and pytest passes for that Python version with no test failures. make test-docker-all is successful only when all four Python versions pass.
This intentionally keeps the Docker scope minimal and aligned with the CI pytest step, without pulling in linting, typing, or benchmark jobs.
See VERSIONS.md for detailed version history and changelog.
See the examples/ directory:
examples/basic_usage.py— Basic CLI argument handlingexamples/enums.py— Enum typesexamples/positional_args.py— Positional argumentsexamples/list_args.py— List argumentsexamples/env_vars.py— Environment variable fallbackexamples/subcommands.py— Git-style subcommandsexamples/custom_converters.py— Custom type converters (per-arg and global registry)
MIT-NORUS License — see LICENSE file for details.
Volodymyr Hotsyk — https://github.com/hotsyk