Skip to content

yeetr vs. typer

Typer is a mature, feature-rich CLI framework and a direct inspiration for yeetr — the Annotated[..., Arg/Opt] metadata pattern, path validators, and envvar fallback all take cues from typer. yeetr is a much smaller library aimed at a narrower slice of the problem. Quick honest comparison so you can pick the right tool:

Topic yeetr typer
Style Plain function signature, no decorators Decorators (@app.command()) or typer.run
Zero-boilerplate runner yeet main.py [func] [args...] script — no if __name__ == "__main__" / yeetr.run(...) block needed Similar: typer main.py run [args...] runs scripts, even ones that don't import typer; in code you need typer.run(...) or an @app.command() entry point
Executable shebang #!yeet or run yeet can make the script itself executable without extra wrapper code; FUNC dispatches to a specific function No shebang equivalent: the typer command needs its run subcommand between the file and the args, which a shebang can't inject
Arg vs. option mapping Python's * separator decides: before * = positional args, after * = --options Presence of a default decides: no default = positional arg, default = --option; override per parameter via typer.Argument(...) / typer.Option(...)
Per-param metadata Annotated[T, Arg(...)] / Annotated[T, Opt(...)] Annotated[T, typer.Argument(...)] / typer.Option(...)
Custom parsers Arg/Opt(parser=...) — the CLI string is first coerced to the parser's annotated input type (path validators compose), then the parser runs typer.Argument/Option(parser=...) — the parser receives the raw string
Structured arg object Accept one dataclass or NamedTuple; fields become the CLI and the function receives the object Declare command params individually, then construct your object inside the command
Variadic positional args Native *args: T maps to a trailing variadic positional arg Use list[T] with typer.Argument(...)
Boolean flags One flag, driven by the default: = False -> --flag, = True -> --no-flag Auto-generates the pair --flag / --no-flag; explicit declaration only to customize the names
Subcommands Subcommand-style dispatch via the runner: yeet main.py FUNC [args...] picks any public function by name (also yeet FILE:FUNC and FUNC shebangs); no command groups, nesting, or function listing in --help First-class subcommands, command groups, nested apps
Async functions Native: async def is run via asyncio.run / uvloop.run Not built-in; an async def command is silently never awaited — wrap with asyncio.run(...) yourself
Shell completion Not built-in Built-in (bash/zsh/fish/PowerShell)
Help rendering Rich tables for args and options Rich-formatted help via rich
Type-checker friendliness Pyright-strict clean end-to-end; Arg/Opt are plain, precisely-typed dataclasses Also clean under strict Pyright; typer.Argument()/typer.Option() return Any, which relaxes checking at the parameter site
Logging Rich logging set up by default (opt-out) Not opinionated about logging
Dependencies rich, rich-argparse (small footprint) click, rich, shellingham, typing-extensions
Maturity / ecosystem New and small Widely adopted, large ecosystem
Best for Single-purpose scripts and tools where the function is the CLI Multi-command CLIs, distributed apps, anything needing completion

If you need nested command groups or shell completion, use typer. If you want one function = one CLI with minimal ceremony and strict typing — with yeet FILE FUNC covering the simple multi-command case — yeetr is designed for that.