CLI tool
swansonk14/typed-argument-parser avatar
swansonk14/typed-argument-parser

Tap: a typed argument parser where the class is the schema

Typed argument parser for Python

631 stars51 forksPythonMIT

At a glance

What is it?
Tap is a typed modernisation of Python's argparse, where a dataclass-like subclass declares the arguments, so the parser, the type checker and the editor all read the same definition. It suits a Python team that wants argument definitions to be checkable and navigable, and it is the wrong choice if you need argparse's exact behaviour or a parser that is built at run time from strings.
Who is it for?
Tap fits a Python team that has outgrown hand-written argparse calls and wants the argument schema to be visible to a type checker and to editors, since the same subclass declares the arguments, the help text and the parsed result. It is the wrong choice for a script whose arguments are only known at run time, and it is the wrong choice for a project that needs argparse's specific behaviours, because Tap is a reimplementation rather than a wrapper.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 55 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The argument list is a class, and that is the whole idea

Tap is described as a typed modernisation of Python's argparse, and the argument list becomes a class body rather than a sequence of add_argument calls.

The example in the project documentation makes the difference concrete. The Tap version declares a subclass with annotated attributes:

python
from tap import Tap

class SimpleArgumentParser(Tap):
    name: str  # Your name
    language: str = 'Python'  # Programming language
    package: str = 'Tap'  # Package name
    stars: int  # Number of stars
    max_stars: int = 5  # Maximum stars

args = SimpleArgumentParser().parse_args()

And the argparse equivalent of the same program needs a parser, five separate add_argument calls with explicit type, required and default keywords, and a help string for each one. The Tap version is shorter and the difference is not only length. A name declared as name: str with a trailing comment becomes both the flag definition and the help text.

The stated benefits are static type checking, code completion, and source code navigation such as go to definition and go to implementation. That third one is the one that matters most in practice. In an argparse program the link between the command line and the code that uses the value is a string, so a rename is a search-and-replace and a typo is a runtime error. In a Tap program the link is a class attribute, so a rename is a rename, and a jump to the declaration is a jump to the declaration.

The project was presented at PyCon 2020, and a poster describing the concepts that guided the development is linked from the readme.

Arguments, defaults and positionals from annotations

The rules for turning class attributes into arguments are short. Variables defined as name: type are required arguments, and variables defined as name: type = value are not required and default to the provided value. Positional arguments are specified using the Positional type annotation.

That is the whole declaration model, and it is where the type system does the work. A float annotation produces a float, an int annotation produces an int, and a default of 5 with an int annotation gives an int default rather than whatever type the literal happens to infer.

The table of contents lists the type coverage, and it is more than the basics. There are sections for str, int and float, for bool, for Optional, List, Set, Tuple, Literal and Union, and a section for complex types. The readme also documents an Ignore attribute, which is how you keep a class attribute that is not a command line argument, a reproducer info block, conversion to and from dictionaries, saving and loading arguments, and loading arguments from configuration files.

The last of those is worth a note. A parser that can round-trip its arguments through a dict and reload them from a config file is doing more than argparse, and it is doing the kind of thing that a team wants when it moves to a service that takes a config file rather than flags. The documentation treats these as first-class sections rather than afterthoughts, which is a signal about how the project is scoped.

The reproducibility info block is the other feature that stands out. It is a mechanism for recording the arguments alongside a run, which is the sort of thing a research pipeline or a data processing job needs and which is tedious to build on top of a parser that only returns a namespace.

tapify, and the difference between flags and a function signature

As of version 1.8.0, Tap includes tapify, which runs functions or initializes classes with arguments parsed from the command line. The example is a function with a type annotation and nothing else:

python
# square.py
from tap import tapify

def square(num: float) -> float:
    return num ** 2

if __name__ == '__main__':
    print(f'The square of your number is {tapify(square)}.')

Running python square.py --num 2 prints The square of your number is 4.0. The float annotation on the parameter is what makes --num parse as a float, and the function is then called with it.

This is a different relationship with the command line from the class-based approach, and the documentation has a section on it. The class approach means you write a class and then ask for the arguments; the tapify approach means you write a function and the arguments arrive as its parameters. The documentation covers functions, classes, dataclasses and pydantic models, and has a section on tapify help.

There is also a section on known args, which is the mechanism for a function that wants to inspect the raw argument namespace rather than have it injected. And there is a section on explicit boolean arguments, which is a genuine problem in every command line library: a boolean flag cannot tell you whether the user typed the flag or whether the default was used, so a library has to invent a convention for representing the third state.

The documentation also has a page on converting a Tap class to another form with to_tap_class, with simple and complex examples, which is the path for a project that wants to keep its existing class structure and get the parsing behaviour out of it.

Packaging, the Python floor, and a single runtime dependency

Tap requires Python 3.10 or newer, and the package metadata lists classifiers from 3.10 through 3.14. It is MIT licensed, with the licence text in LICENSE.txt, and the version is dynamic, read from the tap.__version__ attribute at build time rather than written in the metadata.

Installation from PyPI is a single command:

shell
pip install typed-argument-parser

The interesting part of the dependency list is how short it is. The only runtime dependency is docstring-parser at 0.15 or newer, which is there to read docstrings for help text. Everything else is standard library.

That single dependency is a design statement. Tap is not wrapping a third-party parsing framework, it is a reimplementation of argparse that happens to use one small library to extract documentation from your own docstrings. The consequence for an adopter is that the package does not drag in a dependency tree, and the consequence for the project is that it has to implement the parsing behaviour itself, which is also why the readme notes the advantages of being Python-native in terms of overwriting built-in methods such as process_args, adding custom methods, and inheriting from your own template classes.

The development setup uses uv rather than pip for the project's own tooling. Cloning and running uv sync --no-dev installs the package for use, uv sync installs it with development requirements, and the three day-to-day commands are uv run ruff check, uv run ruff format and uv run pytest. The ruff configuration sets a line length of 121, targets py310, and selects the E, F, I and W rule sets with E501, E721 and E731 ignored. There is a separate dependency group without pydantic and one with pydantic at 2.5.0 or newer, which is how the project tests the optional pydantic integration separately from the core.

Where Tap is the wrong choice

The first limitation is that Tap is a reimplementation, not a wrapper, and it will not be argparse in every detail. The readme's own framing is that Tap is a typed modernisation, and the equivalence it demonstrates is in the common case: required and optional arguments, types, defaults and help strings. The cases argparse handles through APIs that are not demonstrated here, such as the custom help formatter, the mutually exclusive group mechanics, or the add_subparsers behaviour in full, are where you would find out whether Tap is compatible with the code you are porting. Plan to port one entry point and read the output carefully rather than swapping the import.

The second is that the declaration model requires the arguments to be known at class definition time. A parser that is built from a list of strings read from a config file at run time is not something Tap supports, and argparse does. That rules out a category of tools, and it is a category that includes anything that exposes a generic command line interface for a family of dynamically named operations.

The third is that the class-based approach is more ceremony for a small script. A four-line script with two flags does not need a class, a type checker and a parser framework, and Tap will read as over-engineering to anyone who has not been bitten by the stringly-typed argparse pattern. The library is aimed at codebases with real entry points, not throwaway scripts.

The fourth is the dependency on docstring-parser for help text. It is a small and reasonable dependency, but it means the help output depends on how your docstrings are formatted, and a project that has never bothered with docstrings will find the help text thinner than argparse's explicit help arguments would have given.

The fifth is the release pace. The tagged versions are v_1.12.0 from 2026-03-28, v_1.11.0 from 2025-09-05 and v_1.10.1 from 2024-07-07, so roughly one release a year with a gap of more than a year between 1.11 and 1.12. The last push was on 2026-08-29. For a library with a single runtime dependency and a stable API, that is a low churn rate, and it is worth checking the issue tracker before committing a project's command line interface to it.

Editorial conclusion

Tap fits a Python team that has outgrown hand-written argparse calls and wants the argument schema to be visible to a type checker and to editors, since the same subclass declares the arguments, the help text and the parsed result. It is the wrong choice for a script whose arguments are only known at run time, and it is the wrong choice for a project that needs argparse's specific behaviours, because Tap is a reimplementation rather than a wrapper. Before adopting it, convert one entry point to a Tap subclass and run uv run pytest, and note that the last push was on 2026-08-29 with the newest release v_1.12.0 from 2026-03-28.

Frequently asked questions

What is Tap and how does it differ from argparse?

Tap is a typed modernisation of Python's argparse. Instead of a series of add_argument calls, you subclass Tap and declare the arguments as annotated class variables, which gives you static type checking, code completion and source code navigation, since the parser, the type checker and the editor all read the same declaration.

How do I install Tap?

Tap requires Python 3.10 or newer. Install it from PyPI with pip install typed-argument-parser, or from source with git clone, cd typed-argument-parser and uv sync --no-dev. For development, uv sync installs the dev requirements, and the commands are uv run ruff check, uv run ruff format and uv run pytest.

How do I declare a required argument and a positional argument in Tap?

A class variable defined as name: type is a required argument, one defined as name: type = value is optional with that default, and a positional argument is declared using the Positional type annotation. The annotation also determines how the value is parsed.

What does tapify do in Tap?

Added in version 1.8.0, tapify runs a function or initialises a class with arguments parsed from the command line, so a function such as def square(num: float) -> float is called with --num 2 giving 4.0. The documentation covers functions, classes, dataclasses and pydantic models.

What does Tap depend on at run time?

Only docstring-parser at 0.15 or newer, which is used to read docstrings for help text. The package is MIT licensed, requires Python 3.10 or newer, and its last push was on 2026-08-29 with the newest release v_1.12.0 from 2026-03-28.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. swansonk14/typed-argument-parser on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/swansonk14-typed-argument-parser.svg)](https://hysenlabs.com/projects/swansonk14-typed-argument-parser)

Was this useful?