<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mauricio Villegas</title>
    <description>The latest articles on DEV Community by Mauricio Villegas (@mauvilsa).</description>
    <link>https://dev.to/mauvilsa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3516167%2F823f9da2-d5dc-49be-8b8b-c3d7ed489268.jpeg</url>
      <title>DEV Community: Mauricio Villegas</title>
      <link>https://dev.to/mauvilsa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mauvilsa"/>
    <language>en</language>
    <item>
      <title>Tab completion, without writing a completion script</title>
      <dc:creator>Mauricio Villegas</dc:creator>
      <pubDate>Thu, 20 Aug 2026 16:45:24 +0000</pubDate>
      <link>https://dev.to/mauvilsa/tab-completion-without-writing-a-completion-script-4nab</link>
      <guid>https://dev.to/mauvilsa/tab-completion-without-writing-a-completion-script-4nab</guid>
      <description>&lt;p&gt;The &lt;a href="https://dev.to/mauvilsa/from-api-client-to-cli-without-writing-a-parser-3h01"&gt;first post&lt;/a&gt; derived a whole command line interface from a client class, and the &lt;a href="https://dev.to/mauvilsa/a-cli-that-works-from-anywhere-5hf0"&gt;second&lt;/a&gt; turned it into a command that runs from any directory. Both left the same gap in place: the tool knows a great deal that its user does not.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;order_by&lt;/code&gt; is one of four strings. &lt;code&gt;distance_unit&lt;/code&gt; is one of two. &lt;code&gt;level&lt;/code&gt; is one of five. The client says so in its type hints, and jsonargparse enforces every one of them, to the point of refusing the run when you get it wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes search &lt;span class="nt"&gt;--order_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;depth
&lt;span class="go"&gt;error: Parser key "order_by":
  Expected a typing.Literal['time', 'time-asc', 'magnitude', 'magnitude-asc']. Got value: depth
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is correct, and arrives too late to be of much use. The alternatives on offer are to remember the four values or to stop and read &lt;code&gt;quakes search --help&lt;/code&gt;. Neither is what you want at forty characters into a command line.&lt;/p&gt;

&lt;p&gt;Tab completion is where that knowledge belongs, and there is nothing to invent for it. The parser has the choices. The shell has a mechanism for asking. All that is missing is the translation between the two, and it is one dependency and one line of code away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that is normally the work
&lt;/h2&gt;

&lt;p&gt;A shell completion is a script in the shell's own language, registered against a command name. Writing one by hand is a small, unpleasant project: bash wants a function that fills &lt;code&gt;COMPREPLY&lt;/code&gt;, zsh wants &lt;code&gt;_arguments&lt;/code&gt; specs in a file whose first line is &lt;code&gt;#compdef&lt;/code&gt;, fish wants a pile of &lt;code&gt;complete -c&lt;/code&gt; calls, tcsh wants a single &lt;code&gt;complete&lt;/code&gt; statement built out of &lt;code&gt;c/&lt;/code&gt;, &lt;code&gt;n/&lt;/code&gt; and &lt;code&gt;p/&lt;/code&gt; patterns. Four dialects, none of which resemble the others.&lt;/p&gt;

&lt;p&gt;Worse than writing them once is keeping them. The completion script is a copy of the interface expressed somewhere else, and it goes stale the moment you add an option and forget. Plenty of tools ship a completion that is a version or two behind what the tool accepts.&lt;/p&gt;

&lt;p&gt;None of that is necessary when the parser can be inspected. &lt;a href="https://github.com/iterative/shtab" rel="noopener noreferrer"&gt;shtab&lt;/a&gt; walks an &lt;code&gt;argparse&lt;/code&gt; parser and writes the script for you, and jsonargparse hands it a parser that already knows the choices, the paths and the types, because those came out of the client's signatures. So the completion ends up derived from the same source as everything else in this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  One extra, one line
&lt;/h2&gt;

&lt;p&gt;shtab is an optional dependency of jsonargparse, declared as the &lt;code&gt;shtab&lt;/code&gt; extra. In &lt;a href="//pyproject.toml"&gt;pyproject.toml&lt;/a&gt; it goes next to the extra that was already there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="py"&gt;"jsonargparse[signatures,shtab]&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.51&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Installing shtab is enough for the machinery to exist, but not for the command to expose it. The argument that prints the script is opt-in, because not every program wants an extra option in its help. Turning it on is a module level call in &lt;a href="//quakes_cli.py"&gt;quakes_cli.py&lt;/a&gt;, next to the &lt;code&gt;register_type&lt;/code&gt; from part 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;jsonargparse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;auto_cli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;set_parsing_settings&lt;/span&gt;

&lt;span class="c1"&gt;# Add --print_completion, which writes the shell completion script for the CLI.
&lt;/span&gt;&lt;span class="nf"&gt;set_parsing_settings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;add_print_completion_argument&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;set_parsing_settings&lt;/code&gt; is jsonargparse's dial for behaviours that apply to every parser in the process rather than to one argument. There is now one more option in the help:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;span class="go"&gt;options:
  -h, --help            Show this help message and exit.
  --config CONFIG       Path to a configuration file.
  --print_config [=flags]
                        Print the configuration after applying all other
                        arguments and exit. ...
  --print_completion {shtab-bash,shtab-zsh,shtab-tcsh,shtab-fish}
                        Print shell completion script.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what did not happen. &lt;a href="//quakes_client.py"&gt;quakes_client.py&lt;/a&gt; was not touched, no argument was annotated with a completer, and nothing anywhere said that &lt;code&gt;order_by&lt;/code&gt; has four values. That was said once, in the client's signature, and this is the third thing generated from it after the parser and the help.&lt;/p&gt;

&lt;p&gt;If the tool is not yours to edit, the same argument can be added from outside with an environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;JSONARGPARSE_ADD_PRINT_COMPLETION_ARGUMENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;some-other-tool &lt;span class="nt"&gt;--print_completion&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;shtab-bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Trying it in bash
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--print_completion&lt;/code&gt; writes a script to standard output and exits. To try it in the shell you are sitting in, run it and evaluate the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;quakes &lt;span class="nt"&gt;--print_completion&lt;/span&gt; shtab-bash&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That lasts until the shell closes, which is what you want while deciding whether any of this is worth keeping. Subcommands complete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes &amp;lt;TAB&amp;gt;&amp;lt;TAB&amp;gt;
&lt;span class="go"&gt;count    event    feed     search   summary
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes su&amp;lt;TAB&amp;gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single dash is enough to ask for options, and which options you are offered depends on where you are. Before a subcommand, the ones that configure the client, which is exactly the set that part 1 got out of &lt;code&gt;__init__&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes -&amp;lt;TAB&amp;gt;&amp;lt;TAB&amp;gt;
&lt;span class="go"&gt;--config         --help           --print_config   -h
--distance_unit  --min_magnitude  --timeout
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a subcommand, that subcommand's own, which is the set that came out of the method. &lt;code&gt;--order_by&lt;/code&gt; and &lt;code&gt;--limit&lt;/code&gt; belong to &lt;code&gt;search&lt;/code&gt; and are not offered under &lt;code&gt;count&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes search &lt;span class="nt"&gt;--&lt;/span&gt;&amp;lt;TAB&amp;gt;&amp;lt;TAB&amp;gt;
&lt;span class="go"&gt;--area            --area.longitude  --end             --max_magnitude   --print_config
--area.help       --area.radius     --help            --min_magnitude   --start
--area.latitude   --config          --limit           --order_by

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes count &lt;span class="nt"&gt;--&lt;/span&gt;&amp;lt;TAB&amp;gt;&amp;lt;TAB&amp;gt;
&lt;span class="go"&gt;--area            --area.longitude  --end             --min_magnitude
--area.help       --area.radius     --help            --print_config
--area.latitude   --config          --max_magnitude   --start
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That split is not something shtab was told about. It falls out of the parser having a subparser per method, which fell out of the client having a method per subcommand.&lt;/p&gt;

&lt;p&gt;And values complete, which is the part that pays for the exercise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes search &lt;span class="nt"&gt;--order_by&lt;/span&gt; &amp;lt;TAB&amp;gt;&amp;lt;TAB&amp;gt;
&lt;span class="gp"&gt;Expected type: Literal['time', 'time-asc', 'magnitude', 'magnitude-asc'];&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;4/4 matched choices
&lt;span class="go"&gt;magnitude      magnitude-asc  time           time-asc
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes search &lt;span class="nt"&gt;--order_by&lt;/span&gt; m&amp;lt;TAB&amp;gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes search &lt;span class="nt"&gt;--order_by&lt;/span&gt; magnitude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The line above the choices is jsonargparse rather than shtab, and it is the reason a generated completion can end up better than a hand written one. Pressing tab twice asks the shell to list what it has; jsonargparse takes the chance to also print the declared type and how much of it your prefix still matches, in colour, on standard error, without disturbing the line you are typing. For an argument with choices that is a convenience. For one without, it is the whole answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes search &lt;span class="nt"&gt;--limit&lt;/span&gt; &amp;lt;TAB&amp;gt;&amp;lt;TAB&amp;gt;
&lt;span class="go"&gt;Expected type: int

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes search &lt;span class="nt"&gt;--start&lt;/span&gt; &amp;lt;TAB&amp;gt;&amp;lt;TAB&amp;gt;
&lt;span class="gp"&gt;Expected type: date | null;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;0/1 matched choices
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--start&lt;/code&gt; is the &lt;code&gt;date&lt;/code&gt; that part 1 taught the parser with &lt;code&gt;register_type&lt;/code&gt;. There is no list of dates to offer, so the type is what gets shown, and the &lt;code&gt;1&lt;/code&gt; counts the one value it can complete, the &lt;code&gt;null&lt;/code&gt; that the type names.&lt;/p&gt;

&lt;p&gt;Arguments whose type is a path complete as paths, because shtab is told so from the type rather than from a hint written by hand. &lt;code&gt;--config&lt;/code&gt; is the clearest case, since the client does not declare it at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes &lt;span class="nt"&gt;--config&lt;/span&gt; &amp;lt;TAB&amp;gt;&amp;lt;TAB&amp;gt;
&lt;span class="go"&gt;config.yaml  config_japan.yaml  data/
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most interesting type in the client goes one level deeper. &lt;code&gt;area&lt;/code&gt; is the &lt;code&gt;Area&lt;/code&gt; dataclass behind an &lt;code&gt;Area | None&lt;/code&gt; hint, and part 1 set its fields from the command line as &lt;code&gt;--area.latitude&lt;/code&gt; and friends. Those complete too, dot and all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes search &lt;span class="nt"&gt;--area&lt;/span&gt;.&amp;lt;TAB&amp;gt;&amp;lt;TAB&amp;gt;
&lt;span class="go"&gt;--area.help       --area.latitude   --area.longitude  --area.radius

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes search &lt;span class="nt"&gt;--area&lt;/span&gt;.latitude &amp;lt;TAB&amp;gt;&amp;lt;TAB&amp;gt;
&lt;span class="go"&gt;Expected type: float
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing about the nesting was declared anywhere either. Three annotated fields on the dataclass become three more options on the parser, and three more candidates in the completion, each carrying its own type. &lt;code&gt;--area.help&lt;/code&gt; is in that list because it is a real option, the one part 1 used to print the dataclass's own help page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it stick
&lt;/h2&gt;

&lt;p&gt;Typing the &lt;code&gt;eval&lt;/code&gt; by hand is fine for an afternoon. The question is where to put it so that it is simply there.&lt;/p&gt;

&lt;p&gt;The tempting answer is to generate the script once, save it, and source the file. For a project you are working on, that is the wrong shape. The install is editable; the whole point of it is that the code changes under a command that stays where it is. A completion script written at setup time starts drifting from the interface on the second afternoon, and drifting quietly, which is the failure mode that made hand written completions unpleasant in the first place.&lt;/p&gt;

&lt;p&gt;So keep the &lt;code&gt;eval&lt;/code&gt; and move it somewhere that runs. In a virtual environment the natural place follows from what a virtual environment is: activating it is the moment &lt;code&gt;quakes&lt;/code&gt; appears on the &lt;code&gt;PATH&lt;/code&gt;, and that is the moment the shell should learn how to complete it. &lt;code&gt;activate&lt;/code&gt; is a plain script that gets sourced, so appending to it is all it takes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VIRTUAL_ENV&lt;/span&gt;&lt;span class="s2"&gt;/bin/activate"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'

# Shell completion for the quakes command.
eval "&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;quakes &lt;span class="nt"&gt;--print_completion&lt;/span&gt; shtab-bash&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From then on the completion is regenerated from the current code every time you activate, and there is nothing to keep in sync:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
&lt;span class="gp"&gt;(venv) $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes search &lt;span class="nt"&gt;--order_by&lt;/span&gt; &amp;lt;TAB&amp;gt;&amp;lt;TAB&amp;gt;
&lt;span class="gp"&gt;Expected type: Literal['time', 'time-asc', 'magnitude', 'magnitude-asc'];&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;4/4 matched choices
&lt;span class="go"&gt;magnitude      magnitude-asc  time           time-asc
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things this does not do. &lt;code&gt;deactivate&lt;/code&gt; does not undo it, so the completion stays registered for the rest of the session; harmless, since it only fires for a command that is no longer on the &lt;code&gt;PATH&lt;/code&gt;. And &lt;code&gt;python -m venv&lt;/code&gt; writes &lt;code&gt;activate&lt;/code&gt; fresh, so recreating the environment loses the appended block. Worth keeping the &lt;code&gt;cat &amp;gt;&amp;gt;&lt;/code&gt; above in a script you rerun rather than in your shell history.&lt;/p&gt;

&lt;p&gt;For a tool you installed rather than one you are writing, the same line goes in &lt;code&gt;~/.bashrc&lt;/code&gt; instead, and the reasoning is unchanged: the completion is generated from whatever version is installed today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or a file, when the startup cost matters
&lt;/h2&gt;

&lt;p&gt;There is a cost to the above, and it is the one thing a saved script buys back. Every &lt;code&gt;eval&lt;/code&gt; starts a Python interpreter, imports jsonargparse, builds the parser out of the client's signatures and walks it. Here that is about a quarter of a second:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;time &lt;/span&gt;quakes &lt;span class="nt"&gt;--print_completion&lt;/span&gt; shtab-bash &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="go"&gt;real    0m0.276s
user    0m0.223s
sys 0m0.042s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once per &lt;code&gt;source venv/bin/activate&lt;/code&gt; is nothing. Once per terminal you open is noticeable, and it is not once: put four tools in your &lt;code&gt;~/.bashrc&lt;/code&gt; this way and you have added a second to every shell. That is the point at which to generate the file instead and let the shell load it, which is also the right thing for a tool other people install, since they gain nothing from paying for a generation step on a program that only changes when they upgrade it.&lt;/p&gt;

&lt;p&gt;Each shell has a directory for exactly this. bash looks in &lt;code&gt;/etc/bash_completion.d/&lt;/code&gt; and, with the &lt;a href="https://github.com/scop/bash-completion" rel="noopener noreferrer"&gt;bash-completion&lt;/a&gt; package, in &lt;code&gt;~/.local/share/bash-completion/completions/&lt;/code&gt;, under the command's name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;quakes &lt;span class="nt"&gt;--print_completion&lt;/span&gt; shtab-bash &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.local/share/bash-completion/completions/quakes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;zsh loads from any directory on &lt;code&gt;$fpath&lt;/code&gt;, from a file named &lt;code&gt;_quakes&lt;/code&gt; whose first line is &lt;code&gt;#compdef quakes&lt;/code&gt;, which is what shtab writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;quakes &lt;span class="nt"&gt;--print_completion&lt;/span&gt; shtab-zsh &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /usr/local/share/zsh/site-functions/_quakes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;fish reads &lt;code&gt;~/.config/fish/completions/quakes.fish&lt;/code&gt;, and needs no configuration line anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;quakes &lt;span class="nt"&gt;--print_completion&lt;/span&gt; shtab-fish &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.config/fish/completions/quakes.fish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;tcsh has no such directory; the file goes wherever you like and &lt;code&gt;~/.tcshrc&lt;/code&gt; sources it.&lt;/p&gt;

&lt;p&gt;The trade is the obvious one. Nothing is generated at startup, and nothing notices when the interface changes, so the generation has to happen somewhere else: an installer step, a &lt;code&gt;make&lt;/code&gt; target, whatever the project already runs. That is the same discipline a hand written completion needs, minus the writing.&lt;/p&gt;

&lt;p&gt;One detail to watch either way. The script registers itself against the program name the parser saw, so running &lt;code&gt;python quakes_cli.py --print_completion shtab-bash&lt;/code&gt; ends the script with &lt;code&gt;complete -o filenames -F _shtab_quakes_cli_py quakes_cli.py&lt;/code&gt;, which completes a file path rather than a command. Generate from the installed command, so that the name in the script is the name people type.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other three shells
&lt;/h2&gt;

&lt;p&gt;Everything above is &lt;code&gt;shtab-bash&lt;/code&gt;. The other values of &lt;code&gt;--print_completion&lt;/code&gt; produce scripts in the other shells' languages, out of the same parser, and the differences between them are the shells' own.&lt;/p&gt;

&lt;p&gt;A caveat before the details: bash is the shell I actually use. Everything below was run and its output is real, but it was run by someone who had to look up the syntax first, so take the surrounding judgements as a starting point rather than as advice from a native. If your shell is one of these three and something here is not how it is done, you are right and I am not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;zsh&lt;/strong&gt; gives the most back. It shows the help text next to each candidate, because the parser had it and zsh has somewhere to put it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;% quakes &amp;lt;TAB&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;count    -- Count the events matching the given criteria, without fetching them.
event    -- Get everything the catalog knows about a single event.
feed     -- Get one of the real time feeds of recent events.
search   -- Search the catalog for events matching the given criteria.
summary  -- Summarize the seismic activity of a period as aggregated statistics.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those sentences are the first lines of the docstrings in the client, which have now been through the help, the completion and nowhere else. Options are scoped to the subcommand as they are in bash, and values complete the same way, without the type guidance, which is a bash-only addition.&lt;/p&gt;

&lt;p&gt;The one wrinkle is that zsh does not register a completion by being told the function exists. Its convention is the &lt;code&gt;#compdef&lt;/code&gt; file on &lt;code&gt;$fpath&lt;/code&gt; from the previous section, read by &lt;code&gt;compinit&lt;/code&gt; at startup; anything arriving later has to be attached by hand. So the &lt;code&gt;eval&lt;/code&gt; needs a second line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;quakes &lt;span class="nt"&gt;--print_completion&lt;/span&gt; shtab-zsh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
compdef _shtab_quakes quakes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;activate&lt;/code&gt; is the same file for bash and zsh, so if you use both, branch on &lt;code&gt;$BASH_VERSION&lt;/code&gt; and &lt;code&gt;$ZSH_VERSION&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Shell completion for the quakes command.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASH_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;quakes &lt;span class="nt"&gt;--print_completion&lt;/span&gt; shtab-bash&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ZSH_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;quakes &lt;span class="nt"&gt;--print_completion&lt;/span&gt; shtab-zsh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    compdef _shtab_quakes quakes
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;fish&lt;/strong&gt; also shows descriptions, and asks the least of the four:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes &amp;lt;TAB&amp;gt;
&lt;span class="go"&gt;count    (Count the events matching the given criteria, without fetching them.)
event    (Get everything the catalog knows about a single event.)
feed     (Get one of the real time feeds of recent events.)
search   (Search the catalog for events matching the given criteria.)
summary  (Summarize the seismic activity of a period as aggregated statistics.)

&lt;/span&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes search &lt;span class="nt"&gt;--m&lt;/span&gt;&amp;lt;TAB&amp;gt;
&lt;span class="go"&gt;--max_magnitude  (Only events at or below this magnitude. (type: float | null,…)
--min_magnitude  (Only events at or above this magnitude. Defaults to the valu…)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;fish has no &lt;code&gt;eval&lt;/code&gt;, and does not need one: a script can be piped straight into &lt;code&gt;source&lt;/code&gt;. That line goes in &lt;code&gt;activate.fish&lt;/code&gt;, which is the file fish reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Shell completion for the quakes command.
quakes --print_completion shtab-fish | source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;tcsh&lt;/strong&gt; is the modest one, and the only one where the generated script is worse than the shell can do. Its completions are a single &lt;code&gt;complete&lt;/code&gt; statement, and shtab writes it with the options of every subparser merged into one list, so &lt;code&gt;--level&lt;/code&gt;, which belongs to &lt;code&gt;feed&lt;/code&gt; alone, is offered everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes &amp;lt;TAB&amp;gt;
&lt;span class="go"&gt;count   event   feed    search  summary
&lt;/span&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes search &lt;span class="nt"&gt;--order_by&lt;/span&gt; &amp;lt;TAB&amp;gt;
&lt;span class="go"&gt;magnitude     magnitude-asc time          time-asc
&lt;/span&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes count &lt;span class="nt"&gt;--level&lt;/span&gt; &amp;lt;TAB&amp;gt;
&lt;span class="go"&gt;1.0         2.5         4.5         all         significant
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That flattening is shtab's, not tcsh's. tcsh completions can run a command to produce their candidates, and that command can look at &lt;code&gt;$COMMAND_LINE&lt;/code&gt; and answer differently depending on the subcommand already typed; shtab uses exactly this technique for positional arguments and simply does not for options. It is also worth knowing that a single dash offers &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;h&lt;/code&gt; rather than the long options, so &lt;code&gt;--&lt;/code&gt; is what you want to type before pressing tab. Both are fixable in shtab rather than facts about the shell.&lt;/p&gt;

&lt;p&gt;For the rest, tcsh does what the others do. The only awkwardness is getting the script in without a file: csh's backticks flatten the multi-line &lt;code&gt;complete&lt;/code&gt; statement, so the comment lines and the line continuations have to go before &lt;code&gt;eval&lt;/code&gt; sees it. In &lt;code&gt;activate.csh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Shell completion for the quakes command.
eval `quakes --print_completion shtab-tcsh | grep -v '^#' | tr -d '\\'`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is a coincidence worth pointing out. &lt;code&gt;python -m venv&lt;/code&gt; writes &lt;code&gt;activate&lt;/code&gt;, &lt;code&gt;activate.csh&lt;/code&gt; and &lt;code&gt;activate.fish&lt;/code&gt;; shtab writes bash, zsh, tcsh and fish. The shells Python thinks are worth an activation script are the shells shtab can complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one thing to remember
&lt;/h2&gt;

&lt;p&gt;Whatever you do with it, the script is a snapshot. It is written from the parser at the moment &lt;code&gt;--print_completion&lt;/code&gt; ran, and it does not change afterwards. Add a parameter to &lt;code&gt;EarthquakeCatalog&lt;/code&gt; and the CLI grows an option immediately, because the class is read on every run, while a completion generated yesterday keeps offering yesterday's list.&lt;/p&gt;

&lt;p&gt;Which is why the shape of the question is &lt;em&gt;when do I regenerate&lt;/em&gt;, not &lt;em&gt;where do I keep the file&lt;/em&gt;. Every activation, and you never think about it again for the price of a quarter of a second. Every install, and it is correct for as long as the version is. Once, by hand, in a terminal you have since closed, and it will be wrong and you will not be told.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this leaves you with
&lt;/h2&gt;

&lt;p&gt;Three posts in, the client class has produced the argument parser, the help, the keys a config file may set and now the completions for four shells, and it still does not import jsonargparse. The pattern is the same every time: write the interface honestly in Python, and the things that are usually hand maintained copies of it stop being copies.&lt;/p&gt;

&lt;p&gt;Completion is the clearest case of it, because a hand written one is so plainly a duplicate of something you already wrote down. Whatever library you use, if it can walk your parser, generating the script is worth the afternoon you would otherwise spend writing one that is wrong by next month.&lt;/p&gt;

</description>
      <category>python</category>
      <category>cli</category>
      <category>bash</category>
      <category>shtab</category>
    </item>
    <item>
      <title>A CLI that works from anywhere</title>
      <dc:creator>Mauricio Villegas</dc:creator>
      <pubDate>Mon, 10 Aug 2026 05:12:02 +0000</pubDate>
      <link>https://dev.to/mauvilsa/a-cli-that-works-from-anywhere-5hf0</link>
      <guid>https://dev.to/mauvilsa/a-cli-that-works-from-anywhere-5hf0</guid>
      <description>&lt;p&gt;The &lt;a href="https://dev.to/mauvilsa/from-api-client-to-cli-without-writing-a-parser-3h01"&gt;previous post&lt;/a&gt; turned a client class into a command line tool with a single call to &lt;code&gt;auto_cli&lt;/code&gt;. It ended with a file you run like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python quakes_cli.py search &lt;span class="nt"&gt;--min_magnitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is fine while you are the only user and the terminal happens to be open in the right directory. It stops being fine as soon as it is not: the path has to be spelled out in full, &lt;code&gt;quakes_client.py&lt;/code&gt; has to be next to it so the import works, and jsonargparse has to be installed in whichever Python &lt;code&gt;python&lt;/code&gt; means today. A cron job or a colleague will run into all three.&lt;/p&gt;

&lt;p&gt;What you want instead is a command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes count &lt;span class="nt"&gt;--start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-07-01 &lt;span class="nt"&gt;--min_magnitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
&lt;span class="go"&gt;243
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Available from any directory, with its dependencies installed along with it, and no mention of Python anywhere in the invocation. That takes one new file, &lt;code&gt;pyproject.toml&lt;/code&gt;, and a small change to the code that already exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CLI needs a function
&lt;/h2&gt;

&lt;p&gt;An installed command is a small script that the packaging tool generates and puts on your &lt;code&gt;PATH&lt;/code&gt;. All it does is import something from your code and call it. So there has to be something to call, and a body sitting inside &lt;code&gt;if __name__ == "__main__"&lt;/code&gt; is not it: that block runs when the file is executed as a script, and is skipped when the file is imported — and importing is exactly what the generated script does.&lt;/p&gt;

&lt;p&gt;So the tail of &lt;code&gt;quakes_cli.py&lt;/code&gt; goes from this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;auto_cli&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EarthquakeCatalog&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;CatalogError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Run the command line interface.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;auto_cli&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EarthquakeCatalog&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;CatalogError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The body did not change, only where it lives. The &lt;code&gt;if __name__&lt;/code&gt; block stays, now one line long. Keeping it costs nothing, means the file is still runnable directly while developing, and keeps every command in the first post working as it was written.&lt;/p&gt;

&lt;p&gt;The same applies to your own tools, whether or not you ever package them. Whatever is inside &lt;code&gt;if __name__ == "__main__"&lt;/code&gt; should be one call to a function that lives just above it.&lt;/p&gt;

&lt;h2&gt;
  
  
  pyproject.toml
&lt;/h2&gt;

&lt;p&gt;One file declares what the project is, what it needs, and what it installs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[build-system]&lt;/span&gt;
&lt;span class="py"&gt;requires&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;["setuptools&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;77.0&lt;/span&gt;&lt;span class="s"&gt;"]&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;build-backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"setuptools.build_meta"&lt;/span&gt;

&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"quakes-cli"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Command line client for the earthquake catalog of the U.S. Geological Survey."&lt;/span&gt;
&lt;span class="py"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.10&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="py"&gt;"jsonargparse[signatures]&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.50&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[project.scripts]&lt;/span&gt;
&lt;span class="py"&gt;quakes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"quakes_cli:main"&lt;/span&gt;

&lt;span class="nn"&gt;[tool.setuptools]&lt;/span&gt;
&lt;span class="py"&gt;py-modules&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"quakes_cli"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"quakes_client"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole of it, minus the metadata that every published project also wants and that changes nothing in how any of this works: the readme, the license, the authors, the URLs. &lt;a href="https://github.com/mauvilsa/blog-earthquake-cli/blob/main/pyproject.toml" rel="noopener noreferrer"&gt;The file in the repository&lt;/a&gt; has those as well.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[build-system]&lt;/code&gt; names the tool that turns this directory into something installable. Setuptools is the default choice and the one most people already have; hatchling, flit and pdm are equally valid and differ mostly in the last section of this file.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[project]&lt;/code&gt; is the metadata, standardised in &lt;a href="https://peps.python.org/pep-0621/" rel="noopener noreferrer"&gt;PEP 621&lt;/a&gt; and identical whichever build backend you picked. The two lines doing real work are &lt;code&gt;dependencies&lt;/code&gt; and &lt;code&gt;requires-python&lt;/code&gt;. The former replaces &lt;code&gt;requirements.txt&lt;/code&gt;, which the repository no longer has: what a package needs belongs in the package's own metadata, so that installing the package installs its dependencies too, in a fresh environment, on a machine that never saw this repository. The latter is what stops &lt;code&gt;pip&lt;/code&gt; from installing this on Python 3.9, where the &lt;code&gt;float | None&lt;/code&gt; hints that the client is written with fail on import.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[project.scripts]&lt;/code&gt; is the point of the exercise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;quakes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"quakes_cli:main"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the left, the name the command has in a terminal. On the right, the function it calls, as &lt;code&gt;module:function&lt;/code&gt;. The two are independent, so the command does not have to be named after the module, and one project can declare several commands pointing at different functions. This is also where the &lt;code&gt;main&lt;/code&gt; of the previous section earns its existence.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[tool.setuptools]&lt;/code&gt; is the one backend-specific part. This project is two files at the root of the repository rather than a package directory, so they are declared as &lt;code&gt;py-modules&lt;/code&gt;. That is unusual enough to explain: most projects put their code in a package, both to claim a single name in &lt;code&gt;site-packages&lt;/code&gt; and to have somewhere to grow. Here, &lt;code&gt;quakes_cli&lt;/code&gt; and &lt;code&gt;quakes_client&lt;/code&gt; are specific enough not to collide with anything else installed, and leaving them where the first post put them keeps its snippets running. Adding a third module would be the moment to reconsider.&lt;/p&gt;

&lt;p&gt;Three names show up in that file, which is a common source of confusion. &lt;code&gt;quakes-cli&lt;/code&gt; is the distribution name, the one used to install and the one that has to be unique on PyPI. &lt;code&gt;quakes_cli&lt;/code&gt; is the module. &lt;code&gt;quakes&lt;/code&gt; is the command. They are allowed to differ, and often have to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing it
&lt;/h2&gt;

&lt;p&gt;While working on the project, install it in editable mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="go"&gt;Building wheels for collected packages: quakes-cli
  Building editable for quakes-cli (pyproject.toml): finished with status 'done'
Successfully built quakes-cli
Installing collected packages: quakes-cli
Successfully installed quakes-cli-0.1.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sources stay where they are and the environment points at them, so an edit takes effect on the next run with nothing to reinstall. Drop the &lt;code&gt;-e&lt;/code&gt; and it copies instead, which is what you want when checking that the install itself is correct.&lt;/p&gt;

&lt;p&gt;Either way there is now a &lt;code&gt;quakes&lt;/code&gt; on the &lt;code&gt;PATH&lt;/code&gt;, and it behaves like any other command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;span class="go"&gt;usage: quakes [--config CONFIG] [--distance_unit {km,mi}]
              [--min_magnitude MIN_MAGNITUDE] [--timeout TIMEOUT]
              {count,event,feed,search,summary} ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the usage line. It said &lt;code&gt;quakes_cli.py&lt;/code&gt; before and says &lt;code&gt;quakes&lt;/code&gt; now, and nothing was configured to make that happen: argparse takes the program name from how the program was invoked, so the help follows the command around.&lt;/p&gt;

&lt;p&gt;For people who only want to use the tool, &lt;code&gt;pipx install .&lt;/code&gt; or &lt;code&gt;uv tool install .&lt;/code&gt; are the friendlier form; once the project is published somewhere, the dot becomes the distribution name. Both put the command on the &lt;code&gt;PATH&lt;/code&gt; and keep its dependencies in an environment of their own, so a CLI never has to share an environment with anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Settings that follow the command
&lt;/h2&gt;

&lt;p&gt;Something did not survive the move. The first post ended with the client's settings in a config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;quakes &lt;span class="nt"&gt;--config&lt;/span&gt; config.yaml search &lt;span class="nt"&gt;--start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-07-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That path is resolved relative to the directory you are standing in, which used to be the repository and can now be anywhere. A command that runs from everywhere, but whose settings only work in one directory, is not much of an improvement.&lt;/p&gt;

&lt;p&gt;The fix is one argument. Anything &lt;code&gt;auto_cli&lt;/code&gt; does not recognise is handed to the parser, and the parser accepts a list of places to read configuration from on every run, without &lt;code&gt;--config&lt;/code&gt; having to point at them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;auto_cli&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;EarthquakeCatalog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;default_config_files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;~/.config/quakes.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write the settings there once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ~/.config/quakes.yaml&lt;/span&gt;
&lt;span class="na"&gt;distance_unit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mi&lt;/span&gt;
&lt;span class="na"&gt;min_magnitude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and they apply from every directory, with nothing on the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes count &lt;span class="nt"&gt;--start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-08-01
&lt;span class="go"&gt;113
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file gives defaults, not final values, so &lt;code&gt;--config&lt;/code&gt; still overrides it, and a command line argument overrides both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes &lt;span class="nt"&gt;--min_magnitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6 count &lt;span class="nt"&gt;--start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-08-01
&lt;span class="go"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The help says what is going on, at the top and in the defaults of every option the file touched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quakes &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;span class="go"&gt;usage: quakes [--config CONFIG] [--distance_unit {km,mi}]
              [--min_magnitude MIN_MAGNITUDE] [--timeout TIMEOUT]
              {count,event,feed,search,summary} ...

default config file locations:
  ['~/.config/quakes.yaml'], Note: default values below are the ones
  overridden by the contents of: ~/.config/quakes.yaml
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;  --distance_unit {km,mi}
                        Unit used for all distances, both given and returned.
                        (type: Literal['km', 'mi'], default: mi)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;default: mi&lt;/code&gt; is not what the client's signature says, because the file said otherwise. &lt;code&gt;--help&lt;/code&gt; reports what a run would actually use, not what the code was written with.&lt;/p&gt;

&lt;p&gt;The entries are glob patterns and more than one is allowed, with later files winning over earlier ones. So the layering that well behaved command line tools tend to have costs one keyword argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;default_config_files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/quakes.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;~/.config/quakes.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quakes.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A system wide file, then the user's, then one for the project you happen to be standing in. That first path is the Unix convention and has no Windows equivalent. &lt;code&gt;~/.config&lt;/code&gt; does work on Windows, but no user there expects to find settings in it; if you would rather use each operating system's native location, &lt;a href="https://pypi.org/project/platformdirs/" rel="noopener noreferrer"&gt;platformdirs&lt;/a&gt; computes it, and &lt;code&gt;default_config_files&lt;/code&gt; takes &lt;code&gt;Path&lt;/code&gt; objects as readily as strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this leaves you with
&lt;/h2&gt;

&lt;p&gt;The tool is now something that can be installed rather than a file that can be run. It declares its own dependencies, refuses to install where it would not work, gives its user a command instead of a path, carries its settings around, and can be built into a wheel and put on PyPI or an internal index with &lt;code&gt;python -m build&lt;/code&gt; and &lt;code&gt;twine upload&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The part that carries beyond this project is the smallest one. An entry point needs a function, so write the function and let &lt;code&gt;if __name__ == "__main__"&lt;/code&gt; do nothing but call it.&lt;/p&gt;

</description>
      <category>python</category>
      <category>cli</category>
      <category>packaging</category>
      <category>configuration</category>
    </item>
    <item>
      <title>From API client to CLI, without writing a parser</title>
      <dc:creator>Mauricio Villegas</dc:creator>
      <pubDate>Mon, 03 Aug 2026 04:22:59 +0000</pubDate>
      <link>https://dev.to/mauvilsa/from-api-client-to-cli-without-writing-a-parser-3h01</link>
      <guid>https://dev.to/mauvilsa/from-api-client-to-cli-without-writing-a-parser-3h01</guid>
      <description>&lt;p&gt;At some point most of us write a class that wraps an HTTP API. The constructor takes the things that do not change between calls: the base URL, the credentials, the timeout, a few defaults. The methods are the endpoints.&lt;/p&gt;

&lt;p&gt;Then comes the day you want to call it from a terminal. To check one thing quickly, to put it in a cron job, or to hand it to a colleague who does not write Python. The usual answer is an &lt;code&gt;argparse&lt;/code&gt; layer that spells out every parameter a third time, after the signature and after the docstring. It is tedious to write, and it goes stale the moment someone adds a parameter to the client and forgets the CLI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mauvilsa/jsonargparse" rel="noopener noreferrer"&gt;jsonargparse&lt;/a&gt; can derive that layer from the class itself. The signature already says what the parameters are and what types they take. The docstring already says what they mean. That is all a command line parser needs to know, so &lt;code&gt;auto_cli&lt;/code&gt; reads it from there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;auto_cli&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EarthquakeCatalog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This post builds such a client from scratch, a few lines at a time, and every snippet runs as it is written. The API is the &lt;a href="https://earthquake.usgs.gov/fdsnws/event/1/" rel="noopener noreferrer"&gt;USGS Earthquake Catalog&lt;/a&gt;, the service of the U.S. Geological Survey that publishes every seismic event recorded around the world, updated every minute. It needs no key, no account and no registration, so you can paste the code and get real data back. The catalog is live, so the numbers you get will not be exactly the ones shown here.&lt;/p&gt;

&lt;p&gt;If you already have a client class of your own, it will probably need no change at all beyond making sure its type hints and docstrings are complete. Feel free to read the snippets below as a description of what your own class would do.&lt;/p&gt;

&lt;p&gt;You will need Python 3.10 or later and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"jsonargparse[signatures]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A client small enough to paste
&lt;/h2&gt;

&lt;p&gt;Here is the whole first version. One constructor, one method, and a small private helper that performs the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlencode&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlopen&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EarthquakeCatalog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Client for the earthquake catalog of the U.S. Geological Survey.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_magnitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        Args:
            min_magnitude: Magnitude below which events are ignored by default.
            timeout: Seconds to wait for a response before giving up.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_magnitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;min_magnitude&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_magnitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Count the events recorded over the last few days.

        Args:
            days: How far back to look.
            min_magnitude: Only events at or above this magnitude. Defaults to
                the value given to the client.

        Returns:
            The number of matching events.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/fdsnws/event/1/count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;starttime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;minmagnitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_magnitude&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;min_magnitude&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;min_magnitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://earthquake.usgs.gov&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;urlencode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;geojson&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;jsonargparse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;auto_cli&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;auto_cli&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EarthquakeCatalog&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in the class knows about command lines. The last three lines are the entire interface. Save it as &lt;code&gt;quakes.py&lt;/code&gt; and ask it for help:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python quakes.py &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;span class="go"&gt;usage: quakes.py [--config CONFIG] [--min_magnitude MIN_MAGNITUDE]
                 [--timeout TIMEOUT]
                 {count} ...

Client for the earthquake catalog of the U.S. Geological Survey:
  --min_magnitude MIN_MAGNITUDE
                        Magnitude below which events are ignored by default.
                        (type: float, default: 2.5)
  --timeout TIMEOUT     Seconds to wait for a response before giving up.
                        (type: float, default: 30.0)

  Available subcommands:
                        (required)
    count               Count the events recorded over the last few days.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shape of the class is the shape of the command line. Constructor parameters became options that come before the subcommand, public methods became the subcommands, and the class docstring became the description. Ask about the subcommand and the same thing happened one level down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python quakes.py count &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;span class="go"&gt;usage: quakes.py [options] count [--config CONFIG] [--days DAYS]
                                 [--min_magnitude MIN_MAGNITUDE]

Count the events recorded over the last few days.

options:
  --days DAYS           How far back to look. (type: int, default: 30)
  --min_magnitude MIN_MAGNITUDE
                        Only events at or above this magnitude. Defaults to
                        the value given to the client. (type: float | None,
                        default: null)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line of help text was written once, in the docstring, where it also serves anyone reading the code or the generated API documentation. The types were written once, in the signature, where they also serve the type checker.&lt;/p&gt;

&lt;p&gt;So, how many earthquakes of magnitude 5 or above were there last week?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python quakes.py count &lt;span class="nt"&gt;--days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7 &lt;span class="nt"&gt;--min_magnitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
&lt;span class="go"&gt;67
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;min_magnitude&lt;/code&gt; appears in two places, and means something slightly different in each. Before the subcommand it is the client's standing default; after it, the value for this one call. That distinction was expressed only by where the parameter lives in the class, and it survives into the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python quakes.py &lt;span class="nt"&gt;--min_magnitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6 count &lt;span class="nt"&gt;--days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7
&lt;span class="go"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A second endpoint
&lt;/h2&gt;

&lt;p&gt;Counting is a thin thing to do with a catalog of earthquakes. Let us fetch the events themselves, with a real filter: only those within a given distance of a point on the globe.&lt;/p&gt;

&lt;p&gt;That filter has three numbers that belong together, so it deserves a small record of its own. The events coming back deserve one too. Add &lt;code&gt;from dataclasses import dataclass&lt;/code&gt; and &lt;code&gt;from typing import Literal&lt;/code&gt; to the imports, then these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Area&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Circular region of the globe to restrict a search to.

    Args:
        latitude: Latitude of the center, in degrees, positive towards north.
        longitude: Longitude of the center, in degrees, positive towards east.
        radius: Radius around the center, in kilometers.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;500.0&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Quake&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Summary of a single seismic event.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
    &lt;span class="n"&gt;magnitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;place&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_quake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Quake&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;geometry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coordinates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Quake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromtimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;magnitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;place&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;place&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the method itself, next to &lt;code&gt;count&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;min_magnitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;area&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Area&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;time-asc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;magnitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;magnitude-asc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Quake&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Search the catalog for events matching the given criteria.

        Args:
            days: How far back to look.
            min_magnitude: Only events at or above this magnitude. Defaults to
                the value given to the client.
            area: Only events within this region of the globe.
            order_by: Order in which the events are returned.
            limit: Maximum number of events to return.

        Returns:
            The matching events, at most ``limit`` of them.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;within&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;area&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;within&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;longitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxradiuskm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;area&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/fdsnws/event/1/query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;starttime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;minmagnitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_magnitude&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;min_magnitude&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;min_magnitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;orderby&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;order_by&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;within&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;to_quake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;feature&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;features&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of these parameters are more interesting than a number or a string, and the CLI treats both of them accordingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python quakes.py search &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;span class="go"&gt;usage: quakes.py [options] search [--config CONFIG] [--days DAYS]
                                  [--min_magnitude MIN_MAGNITUDE]
                                  [--area AREA]
                                  [--order_by {time,time-asc,magnitude,magnitude-asc}]
                                  [--limit LIMIT]

options:
  --days DAYS           How far back to look. (type: int, default: 30)
  --min_magnitude MIN_MAGNITUDE
                        Only events at or above this magnitude. Defaults to
                        the value given to the client. (type: float | None,
                        default: null)
  --area.help           Show the help for Area and exit.
  --area AREA           Only events within this region of the globe. (type:
                        Area | None, default: null)
  --order_by {time,time-asc,magnitude,magnitude-asc}
                        Order in which the events are returned. (type:
                        Literal['time', 'time-asc', 'magnitude', 'magnitude-
                        asc'], default: time)
  --limit LIMIT         Maximum number of events to return. (type: int,
                        default: 10)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;order_by&lt;/code&gt; is a &lt;code&gt;Literal&lt;/code&gt;, so its four values became the accepted choices. Anything else is rejected with the list of what is allowed, before a request is made.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;area&lt;/code&gt; is a whole object, so it got a help page of its own, built from the dataclass the same way the main help was built from the client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python quakes.py search &lt;span class="nt"&gt;--area&lt;/span&gt;.help
&lt;span class="go"&gt;usage: quakes.py --area.latitude LATITUDE --area.longitude LONGITUDE
                 [--area.radius RADIUS]

Circular region of the globe to restrict a search to:
  --area.latitude LATITUDE
                        Latitude of the center, in degrees, positive towards
                        north. (required, type: float)
  --area.longitude LONGITUDE
                        Longitude of the center, in degrees, positive towards
                        east. (required, type: float)
  --area.radius RADIUS  Radius around the center, in kilometers. (type: float,
                        default: 500.0)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its fields are set individually, with a dot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python quakes.py search &lt;span class="nt"&gt;--area&lt;/span&gt;.latitude&lt;span class="o"&gt;=&lt;/span&gt;35.68 &lt;span class="nt"&gt;--area&lt;/span&gt;.longitude&lt;span class="o"&gt;=&lt;/span&gt;139.69 &lt;span class="nt"&gt;--area&lt;/span&gt;.radius&lt;span class="o"&gt;=&lt;/span&gt;300 &lt;span class="se"&gt;\&lt;/span&gt;
                    &lt;span class="nt"&gt;--min_magnitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5 &lt;span class="nt"&gt;--days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;365 &lt;span class="nt"&gt;--order_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;magnitude &lt;span class="nt"&gt;--limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing had to be registered or declared for that to work. The parameter is typed &lt;code&gt;Area | None&lt;/code&gt;, and &lt;code&gt;Area&lt;/code&gt; is an ordinary dataclass, which is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Printing what comes back
&lt;/h2&gt;

&lt;p&gt;The command above does work, but what it prints is not pleasant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Quake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;42000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;magnitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;54.164&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;37.4213&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;141.5524&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;us6000resn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;place&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;48 km ENE of Tomioka, Japan&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Quake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;798000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;auto_cli&lt;/code&gt; deliberately does not print anything itself. It returns whatever the method returned and lets you decide, which is the one place where a command line tool does need code of its own. The methods have return type hints, so the decision can be made once, for all of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Render a value returned by a client method as text for a terminal.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;is_dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;render_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Render a list of dataclass instances as a table with aligned columns.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nf"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;widths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ljust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;widths&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Import &lt;code&gt;fields&lt;/code&gt; and &lt;code&gt;is_dataclass&lt;/code&gt; from &lt;code&gt;dataclasses&lt;/code&gt; alongside &lt;code&gt;dataclass&lt;/code&gt;, and wrap the call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;jsonargparse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;auto_cli&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;auto_cli&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EarthquakeCatalog&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A list of records is a table whose columns are the fields of the record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python quakes.py search &lt;span class="nt"&gt;--area&lt;/span&gt;.latitude&lt;span class="o"&gt;=&lt;/span&gt;35.68 &lt;span class="nt"&gt;--area&lt;/span&gt;.longitude&lt;span class="o"&gt;=&lt;/span&gt;139.69 &lt;span class="nt"&gt;--area&lt;/span&gt;.radius&lt;span class="o"&gt;=&lt;/span&gt;300 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;                      --min_magnitude=5 --days=365 --order_by=magnitude --limit=3
time              magnitude  depth   latitude  longitude  id          place
2025-10-04 15:21  5.9        54.164  37.4213   141.5524   us6000resn  48 km ENE of Tomioka, Japan
2026-06-26 03:46  5.8        43      35.6907   140.5771   us6000t8cy  2 km ESE of Yōkaichiba, Japan
2026-06-16 10:46  5.3        63.617  36.1715   139.6995   us7000std7  2 km SW of Koga, Japan
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A number is still just a number, undecorated rather than quoted or wrapped in JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python quakes.py count &lt;span class="nt"&gt;--days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7 &lt;span class="nt"&gt;--min_magnitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
&lt;span class="go"&gt;67
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything else falls through to indented JSON. Adding a method that returns a &lt;code&gt;dict&lt;/code&gt; needs no new printing code, which is the point of dispatching on the type rather than on the subcommand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of the client
&lt;/h2&gt;

&lt;p&gt;The complete version of this client lives in &lt;a href="https://github.com/mauvilsa/blog-earthquake-cli" rel="noopener noreferrer"&gt;this repository&lt;/a&gt;, split into &lt;code&gt;quakes_client.py&lt;/code&gt; for the client and &lt;code&gt;quakes_cli.py&lt;/code&gt; for the interface. It is about three hundred lines across the two files, most of them docstrings. Beyond what is above it has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;event(event_id)&lt;/code&gt;, the full record of one event, returned as the &lt;code&gt;dict&lt;/code&gt; the catalog itself provides.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;feed(level, period)&lt;/code&gt;, the USGS real time feeds, which are precomputed and so the fastest way to see what is happening right now.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;summary(start, end, area)&lt;/code&gt;, aggregated statistics that the client computes out of several requests.&lt;/li&gt;
&lt;li&gt;Real &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;end&lt;/code&gt; dates instead of a &lt;code&gt;days&lt;/code&gt; count.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;distance_unit&lt;/code&gt; on the client that converts both the radius you give and the depths you get back.&lt;/li&gt;
&lt;li&gt;Errors from the API turned into one readable line.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its help is the same shape as the small one, just longer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python quakes_cli.py &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;span class="go"&gt;usage: quakes_cli.py [--config CONFIG] [--distance_unit {km,mi}]
                     [--min_magnitude MIN_MAGNITUDE] [--timeout TIMEOUT]
                     {count,event,feed,search,summary} ...

Client for the earthquake catalog of the U.S. Geological Survey:
  --distance_unit {km,mi}
                        Unit used for all distances, both given and returned.
                        (type: Literal['km', 'mi'], default: km)
  --min_magnitude MIN_MAGNITUDE
                        Magnitude below which events are ignored by default.
                        (type: float, default: 2.5)
  --timeout TIMEOUT     Seconds to wait for a response before giving up.
                        (type: float, default: 30.0)

  Available subcommands:
    count               Count the events matching the given criteria, without
                        fetching them.
    event               Get everything the catalog knows about a single event.
    feed                Get one of the real time feeds of recent events.
    search              Search the catalog for events matching the given
                        criteria.
    summary             Summarize the seismic activity of a period as
                        aggregated statistics.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;event&lt;/code&gt; takes an &lt;code&gt;event_id&lt;/code&gt; with no default, so jsonargparse made it a positional argument rather than an option. If you would rather keep every parameter as a named option, pass &lt;code&gt;as_positional=False&lt;/code&gt; to &lt;code&gt;auto_cli&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;quakes_cli.py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;event&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;us&lt;/span&gt;&lt;span class="mi"&gt;7000&lt;/span&gt;&lt;span class="err"&gt;srb&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us7000srb1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;7.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"place"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"25 km SW of Kablalan, Philippines"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1780875461970&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"updated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1785641718662&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tz"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://earthquake.usgs.gov/earthquakes/eventpage/us7000srb1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"felt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;581&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cdi"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;9.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mmi"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;8.53&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alert"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orange"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reviewed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tsunami"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1529&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"net"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7000srb1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ids"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;",us7000srb1,usauto7000srb1,"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;",us,usauto,"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;",dyfi,finite-fault,general-text,ground-failure,impact-text,internal-moment-tensor,losspager,moment-tensor,origin,phase-data,shakemap,"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nst"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dmin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"magType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mww"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"earthquake"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"M 7.8 - 25 km SW of Kablalan, Philippines"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was the strongest of 2026 at the time of writing. &lt;code&gt;summary&lt;/code&gt; returns a &lt;code&gt;dict&lt;/code&gt; too, and prints the same way without a single line of code added for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;quakes_cli.py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;summary&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--start=&lt;/span&gt;&lt;span class="mi"&gt;2026-07-01&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--end=&lt;/span&gt;&lt;span class="mi"&gt;2026-08-01&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"end"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3379&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"by_magnitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"minor 2.0-3.9"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"light 4.0-4.9"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;919&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moderate 5.0-5.9"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;209&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strong 6.0-6.9"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"major 7.0+"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"strongest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-17 14:48:40.227000+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"magnitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;7.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"depth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"latitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;14.6361&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"longitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-92.8969&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us7000t1bu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"place"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"52 km W of Puerto Madero, Mexico"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full client also uses &lt;code&gt;datetime.date&lt;/code&gt; for &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;end&lt;/code&gt;, which is a type jsonargparse does not handle out of the box. Rather than weakening the client's type hints to &lt;code&gt;str&lt;/code&gt;, the CLI teaches the parser how to read and write that type, once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;register_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deserializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From then on &lt;code&gt;--start=2026-07-01&lt;/code&gt; arrives at the client as a &lt;code&gt;date&lt;/code&gt;, and a malformed one is rejected by the parser instead of by the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The settings you do not want to retype
&lt;/h2&gt;

&lt;p&gt;The client parameters are the ones you would get tired of typing: the endpoint, the timeout, the credentials. Every tool built with &lt;code&gt;auto_cli&lt;/code&gt; accepts a &lt;code&gt;--config&lt;/code&gt; file, so they can be written down once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config.yaml&lt;/span&gt;
&lt;span class="na"&gt;distance_unit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mi&lt;/span&gt;
&lt;span class="na"&gt;min_magnitude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4.0&lt;/span&gt;
&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python quakes_cli.py &lt;span class="nt"&gt;--config&lt;/span&gt; config.yaml search &lt;span class="nt"&gt;--start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-07-01 &lt;span class="nt"&gt;--order_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;magnitude &lt;span class="nt"&gt;--limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;time              magnitude  depth  latitude  longitude  id          place
2026-07-17 14:48  7.3        13.67  14.6361   -92.8969   us7000t1bu  52 km W of Puerto Madero, Mexico
2026-07-28 07:27  6.8        6.21   32.6817   130.7217   us6000tgb9  2026 Uto, Japan Earthquake
2026-07-17 15:20  6.4        6.21   14.216    -93.2025   us7000t1cc  101 km WSW of Puerto Madero, Mexico
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an API that needs authentication this is the natural home for the token or the user name, in a file you can keep out of version control and give restrictive permissions, rather than in your shell history.&lt;/p&gt;

&lt;p&gt;A config file can also cover a subcommand, by nesting its options under the subcommand's name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config_japan.yaml&lt;/span&gt;
&lt;span class="na"&gt;min_magnitude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4.0&lt;/span&gt;

&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-01-01&lt;/span&gt;
  &lt;span class="na"&gt;order_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;magnitude&lt;/span&gt;
  &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;area&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;36.2&lt;/span&gt;
    &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;138.25&lt;/span&gt;
    &lt;span class="na"&gt;radius&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;700&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When it does, the subcommand does not have to be named on the command line either. The file is the whole invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python quakes_cli.py &lt;span class="nt"&gt;--config&lt;/span&gt; config_japan.yaml
&lt;span class="go"&gt;time              magnitude  depth  latitude  longitude  id          place
2026-04-20 07:52  7.4        25     39.971    143.0592   us6000sri7  102 km ENE of Miyako, Japan
2026-06-24 22:30  6.9        34     40.2745   142.1353   us6000t7zq  32 km ENE of Kuji, Japan
2026-05-15 11:22  6.7        42     38.9352   142.1579   us6000sxwq  41 km ESE of Ōfunato, Japan
2026-03-26 14:18  6.5        16     39.4377   143.3826   us7000s7u4  123 km E of Yamada, Japan
2026-07-01 12:08  6          35     40.2085   142.4411   us6000t9ej  54 km ENE of Noda, Japan
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arguments given on the command line still win, so &lt;code&gt;search --limit=2&lt;/code&gt; after that would override the file. And &lt;code&gt;--print_config&lt;/code&gt; dumps the settings a run would use, which is the least tedious way to start a new config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;$ python quakes_cli.py --config config_japan.yaml search --print_config=skip_default&lt;/span&gt;
&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2026-01-01'&lt;/span&gt;
&lt;span class="na"&gt;area&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;36.2&lt;/span&gt;
  &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;138.25&lt;/span&gt;
  &lt;span class="na"&gt;radius&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;700.0&lt;/span&gt;
&lt;span class="na"&gt;order_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;magnitude&lt;/span&gt;
&lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bringing your own client
&lt;/h2&gt;

&lt;p&gt;If you already have a client class, the work is mostly checking that it says about itself what it already knows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every parameter has a type hint, and the honest one. &lt;code&gt;date&lt;/code&gt; rather than &lt;code&gt;str&lt;/code&gt; for a date, &lt;code&gt;Literal&lt;/code&gt; rather than &lt;code&gt;str&lt;/code&gt; for something with four valid values, a small dataclass rather than three loose floats that belong together.&lt;/li&gt;
&lt;li&gt;Every parameter is described in the docstring, in a style &lt;a href="https://pypi.org/project/docstring-parser/" rel="noopener noreferrer"&gt;docstring_parser&lt;/a&gt; understands, which covers the Google, Numpy, Sphinx and Epydoc conventions.&lt;/li&gt;
&lt;li&gt;Methods you do not want as subcommands start with an underscore.&lt;/li&gt;
&lt;li&gt;The constructor takes what does not change between calls. For most real APIs that means the credentials, and a config file already covers them. Passing &lt;code&gt;default_env=True&lt;/code&gt; to &lt;code&gt;auto_cli&lt;/code&gt; adds an environment variable for each one as well, again without the client having to read either.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the whole interface is one call, and it cannot fall out of step with the client, because there is nothing to keep in step.&lt;/p&gt;

&lt;p&gt;The client, meanwhile, stays a plain class. It knows nothing about argparse, YAML, config files or terminals, and it is as pleasant to import from a notebook or a web service as it ever was. That is the part worth keeping: the command line tool is a projection of the class, not a second implementation of it.&lt;/p&gt;

&lt;p&gt;There is more that jsonargparse can do with the same class, including shell completion, deeper trees of subcommands, and dependency injection through type hints. The &lt;a href="https://jsonargparse.readthedocs.io/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; has the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure, and the alternatives
&lt;/h2&gt;

&lt;p&gt;I am the author of jsonargparse, so this post is a showcase of its features and you should read it with that in mind. The idea it rests on, that a command line interface can be derived from what the code already declares, is not mine and not exclusive to this package. Several others do a good part of it, and for a given project one of them may fit better. Here is what they do and where they stop, checked against click 8.4, typer 0.27, fire 0.7 and tyro 1.0.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/google/python-fire" rel="noopener noreferrer"&gt;Fire&lt;/a&gt; is the closest in spirit. &lt;code&gt;fire.Fire(EarthquakeCatalog)&lt;/code&gt; also turns constructor parameters into flags, methods into subcommands and docstrings into help. What it does not do is take the type hints seriously: &lt;code&gt;--days=notanumber&lt;/code&gt; arrives at the method as that string, and &lt;code&gt;--area='{"latitude": 1, "longitude": 2}'&lt;/code&gt; arrives as a plain &lt;code&gt;dict&lt;/code&gt; rather than an &lt;code&gt;Area&lt;/code&gt;. There is no config file support. Fire is built to expose arbitrary Python objects, not objects written to be exposed, so it guesses where jsonargparse validates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/brentyi/tyro" rel="noopener noreferrer"&gt;Tyro&lt;/a&gt; is the closest on types. It builds the parser from hints and docstrings, and its handling of nested structures is excellent: the same &lt;code&gt;area: Area | None&lt;/code&gt; becomes a neat pair of &lt;code&gt;area:area&lt;/code&gt; and &lt;code&gt;area:none&lt;/code&gt; subcommands. It works from a function or a single class, though. &lt;code&gt;tyro.cli(EarthquakeCatalog)&lt;/code&gt; gives you the constructor's options and returns an instance; the methods do not become subcommands. There is also no general &lt;code&gt;--config file.yaml&lt;/code&gt;, only helpers to serialize dataclasses to and from YAML and to pick among configurations defined in code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/fastapi/typer" rel="noopener noreferrer"&gt;Typer&lt;/a&gt; derives options from the hints of functions registered with &lt;code&gt;@app.command()&lt;/code&gt;. &lt;code&gt;Literal&lt;/code&gt; becomes choices and &lt;code&gt;int&lt;/code&gt; is validated, but past the simple types it stops: a dataclass parameter raises &lt;code&gt;RuntimeError: Type not yet supported&lt;/code&gt;. Per-parameter help does not come from the docstring either, it is written again inside an &lt;code&gt;Annotated&lt;/code&gt; hint with &lt;code&gt;typer.Option(help=...)&lt;/code&gt;. In exchange you get click underneath, nicely formatted help and shell completion that installs itself with one flag.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/pallets/click" rel="noopener noreferrer"&gt;Click&lt;/a&gt; is the established one and is explicit on purpose. Every option is a decorator, so nothing is inferred: a parameter with only a type hint produces no option at all, and the docstring becomes the description verbatim, &lt;code&gt;Args:&lt;/code&gt; section and all. That explicitness is an advantage whenever the command line should not have the same shape as the Python API, and nothing matches click's ecosystem or its control over the terminal. It is a cost when the CLI is meant to be the API, which is the case in this post.&lt;/p&gt;

&lt;p&gt;What jsonargparse adds here is the combination: a class with methods becomes a tree of subcommands, arbitrary type hints are validated rather than guessed, and the same tree can be filled from a config file or the environment instead of the command line. And &lt;code&gt;auto_cli&lt;/code&gt; is only one way in. Underneath it is an argparse-like interface where you build the parser yourself and add arguments one at a time, or from a function, a class, a method or a bare type hint, wherever you want them, and link one to another. This post shows a small part of what is there.&lt;/p&gt;

&lt;p&gt;None of which settles the question. For a tool the size of the one built here any of these packages does the job, and the differences that matter tend to surface later: when the interface grows to cover more of an API, when users start wanting their settings in a file, when the types stop being strings and integers. So the advice is the boring kind. Try a couple of them against the tool you expect to have in a year, rather than the one you are writing this afternoon.&lt;/p&gt;

</description>
      <category>python</category>
      <category>cli</category>
      <category>argparse</category>
      <category>typehints</category>
    </item>
  </channel>
</rss>
