Motivation
The trackers/scripts package name is misleading. Its content is a fully structured CLI package: an entry point (__main__.py), four subcommand handlers (track, eval, tune, download), a presentation utility (progress.py), and a registry-driven argument builder that auto-exposes every tracker parameter as a --tracker.<param> flag by reading BaseTracker._registry at parser-construction time.
The current implementation (argparse, ~1 280 lines across six files) works correctly but carries two pain points:
- Name mismatch —
scripts/ implies one-off build utilities; the actual role is a user-facing CLI.
- Verbosity — argparse requires 4–8 lines per argument; no typed handler signatures;
Namespace attributes are invisible to mypy and IDEs.
A rename to trackers/cli/ and an optional light refactor to argparse + dataclasses would improve readability, type safety, and maintainability with zero new dependencies.
Goal
- Rename
trackers/scripts/ → trackers/cli/ and update pyproject.toml console-scripts entry.
- Evaluate whether switching the argument-parsing backend to a third-party CLI framework is worthwhile given the project's strict dependency-weight policy.
- If no third-party framework wins, apply a minimal stdlib-only refactor (typed dataclass per command) to improve handler type safety.
Hard constraint
Any new dependency must itself have zero transitive dependencies (pure Python, no further Requires). The project already avoids heavy optional extras for good reason.
CLI surface (current)
status?
Four subcommands, ~22 static args in track plus N dynamic --tracker.<param> flags generated at runtime from the tracker registry, plus 6–10 args each in eval, tune, download.
Framework comparison
| Framework |
Pkgs |
Depth |
Size |
Registry |
Ergonomics |
Type safety |
Pros |
Cons |
| argparse (current) |
0 |
0 |
stdlib |
✓ native |
baseline; 4–8 lines/arg |
✗ bare Namespace |
stdlib, zero risk |
verbose, no mypy gain |
| argparse + dataclasses |
0 |
0 |
stdlib |
✓ native |
~10–15%; typed handler bodies, same parser boilerplate |
~ handler typed, parser not |
zero risk, mypy gain |
modest improvement only |
| click 8.x |
1 |
0 |
~170 KB |
~ cmd.params.append() |
~30–40%; decorator per arg, reads like --help |
~ runtime ok; mypy weak on decorators |
best ergonomics, mature |
registry awkward, 1 dep |
| fire 0.7 |
1 |
0 |
~208 KB |
✗ **kwargs loses types/choices/help |
~50–60% (static only); function = command |
✗ untyped **kwargs |
lowest boilerplate |
registry broken |
| docopt-ng |
1 |
0 |
~20 KB |
✗ docstring fixed at import |
~50% (static only); docstring = spec |
✗ dict return |
tiny, self-documenting |
registry incompatible, unmaintained lineage |
| typer 0.16 |
4 |
3 |
~8 MB |
✗ fights annotation model |
~40–50%; annotation = spec |
✓ annotations drive both |
best IDE/DX for static cmds |
8 MB, registry hostile |
| jsonargparse 4.48 |
2 |
1 |
~313 KB |
✓ drop-in add_argument |
~40–50%; dataclass/annotation superset |
✓ types drive parsing + typed Namespace |
YAML/JSON config free, argparse superset |
depth 1 via PyYAML, fails strict constraint |
| defopt 7.x |
0 |
0 |
~30 KB |
✗ no parser access |
~40% static-only; function unchanged, docstring = spec |
✓ annotations drive both |
zero deps, no decorators, reuses existing Google/NumPy docstrings |
registry incompatible (**kwargs unsupported, no internal parser exposed), dynamic args impossible |
Package descriptions
- argparse — Python stdlib argument parser (since 3.2). No deps. Verbose but universal; every Python environment already has it.
- dataclasses — Python stdlib decorator (since 3.7) that generates
__init__, __repr__, etc. from annotated fields. Used here as a typed wrapper over argparse.Namespace.
- click 8.x · GitHub — Decorator-based CLI toolkit by Pallets. Single dependency (optionally
colorama on Windows). Mature, widely adopted, excellent --help generation.
- fire 0.7 · Guide — Google library that turns any Python object into a CLI with zero boilerplate. Derives the interface from function/class signatures at runtime; no explicit arg definitions needed.
- docopt-ng — Jazzband-maintained fork of the original docopt. Parses a usage docstring to produce a
dict of arguments. Single small file (~20 KB), zero deps.
- typer 0.16 · GitHub — Built on Click by the FastAPI author; uses Python type annotations as the single source of truth for both parsing and documentation. Pulls in
rich + shellingham (~8 MB total).
- jsonargparse 4.48 · GitHub — Drop-in superset of
argparse that adds YAML/JSON config-file loading, dataclass types, and typed Namespace. Only hard dep is PyYAML; all other extras are opt-in.
- defopt 7.x · GitHub — Zero-dep library that builds an
argparse parser from a function's type annotations and Google/NumPy/Sphinx docstring. No decorators; call defopt.run(func) to expose a function as a CLI command.
Notes
click — depth 0 on Linux/macOS; pulls colorama (25 KB, 0 own deps) on Windows. Registry pattern works via cmd.params.append(click.Option(...)) but loses decorator ergonomics on the most complex command.
typer — click + rich + shellingham + typing-extensions (7 packages, ~8 MB); rich subtree alone 5 MB+ (pygments). Fails dep constraint and fights registry pattern.
jsonargparse — only required dep is PyYAML>=3.13 (PyYAML itself has zero own deps); everything else (jsonschema, omegaconf, fsspec, etc.) is opt-in extras. Drop-in superset — registry-driven parser.add_argument("--tracker.<param>", ...) loop ports verbatim. Key differentiator: action="config" makes track/eval/tune runs expressible as YAML files (e.g. --config sportsmot-bytetrack.yaml) without any config-loading code.
defopt — zero deps, pure Python. Calls defopt.run(func) or defopt.run([f1, f2]) for flat subcommands; nested dicts for hierarchical subcommands. Parses Sphinx/RST, Google-style, and NumPy docstrings natively — our existing Google-style docstrings are already compatible. Builds argparse internally but does not expose the parser, so runtime injection of dynamic --tracker.<param> flags is impossible. **kwargs parameters raise ValueError, ruling out any kwargs-based dynamic arg pattern. Best fit would be purely static commands (eval, download) but the registry-driven track command disqualifies the whole framework.
Recommendation
- Rename
trackers/scripts/ → trackers/cli/ unconditionally — name should reflect role.
- Framework: stay on
argparse; apply a thin dataclasses layer (stdlib) to give each command handler a typed Args dataclass instead of raw Namespace. Zero new deps, mypy gain, ~10–15 % line reduction.
- Biggest win is not the parser swap but extracting
track.py's execution logic (645 of 745 lines are runtime, not arg definitions) into trackers/io/ or a new trackers/runtime/ module. That alone would shrink track.py more than any framework change.
Motivation
The
trackers/scriptspackage name is misleading. Its content is a fully structured CLI package: an entry point (__main__.py), four subcommand handlers (track,eval,tune,download), a presentation utility (progress.py), and a registry-driven argument builder that auto-exposes every tracker parameter as a--tracker.<param>flag by readingBaseTracker._registryat parser-construction time.The current implementation (argparse, ~1 280 lines across six files) works correctly but carries two pain points:
scripts/implies one-off build utilities; the actual role is a user-facing CLI.Namespaceattributes are invisible to mypy and IDEs.A rename to
trackers/cli/and an optional light refactor toargparse + dataclasseswould improve readability, type safety, and maintainability with zero new dependencies.Goal
trackers/scripts/→trackers/cli/and updatepyproject.tomlconsole-scripts entry.Hard constraint
Any new dependency must itself have zero transitive dependencies (pure Python, no further
Requires). The project already avoids heavy optional extras for good reason.CLI surface (current)
status?
Four subcommands, ~22 static args in
trackplus N dynamic--tracker.<param>flags generated at runtime from the tracker registry, plus 6–10 args each ineval,tune,download.Framework comparison
Namespacecmd.params.append()--help**kwargsloses types/choices/help**kwargsdictreturnadd_argumentNamespace**kwargsunsupported, no internal parser exposed), dynamic args impossiblePackage descriptions
__init__,__repr__, etc. from annotated fields. Used here as a typed wrapper overargparse.Namespace.coloramaon Windows). Mature, widely adopted, excellent--helpgeneration.dictof arguments. Single small file (~20 KB), zero deps.rich+shellingham(~8 MB total).argparsethat adds YAML/JSON config-file loading, dataclass types, and typedNamespace. Only hard dep isPyYAML; all other extras are opt-in.argparseparser from a function's type annotations and Google/NumPy/Sphinx docstring. No decorators; calldefopt.run(func)to expose a function as a CLI command.Notes
click — depth 0 on Linux/macOS; pulls
colorama(25 KB, 0 own deps) on Windows. Registry pattern works viacmd.params.append(click.Option(...))but loses decorator ergonomics on the most complex command.typer —
click + rich + shellingham + typing-extensions(7 packages, ~8 MB);richsubtree alone 5 MB+ (pygments). Fails dep constraint and fights registry pattern.jsonargparse — only required dep is
PyYAML>=3.13(PyYAML itself has zero own deps); everything else (jsonschema,omegaconf,fsspec, etc.) is opt-in extras. Drop-in superset — registry-drivenparser.add_argument("--tracker.<param>", ...)loop ports verbatim. Key differentiator:action="config"makes track/eval/tune runs expressible as YAML files (e.g.--config sportsmot-bytetrack.yaml) without any config-loading code.defopt — zero deps, pure Python. Calls
defopt.run(func)ordefopt.run([f1, f2])for flat subcommands; nested dicts for hierarchical subcommands. Parses Sphinx/RST, Google-style, and NumPy docstrings natively — our existing Google-style docstrings are already compatible. Builds argparse internally but does not expose the parser, so runtime injection of dynamic--tracker.<param>flags is impossible.**kwargsparameters raiseValueError, ruling out any kwargs-based dynamic arg pattern. Best fit would be purely static commands (eval, download) but the registry-driventrackcommand disqualifies the whole framework.Recommendation
trackers/scripts/→trackers/cli/unconditionally — name should reflect role.argparse; apply a thindataclasseslayer (stdlib) to give each command handler a typedArgsdataclass instead of rawNamespace. Zero new deps, mypy gain, ~10–15 % line reduction.track.py's execution logic (645 of 745 lines are runtime, not arg definitions) intotrackers/io/or a newtrackers/runtime/module. That alone would shrinktrack.pymore than any framework change.