<?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>Swapping implementations from the command line</title>
      <dc:creator>Mauricio Villegas</dc:creator>
      <pubDate>Wed, 16 Sep 2026 05:37:29 +0000</pubDate>
      <link>https://dev.to/mauvilsa/swapping-implementations-from-the-command-line-20oh</link>
      <guid>https://dev.to/mauvilsa/swapping-implementations-from-the-command-line-20oh</guid>
      <description>&lt;p&gt;The tool built over the last four posts asks the catalog the same questions again and again. &lt;code&gt;summary&lt;/code&gt; alone is six requests, one per magnitude band plus one for the strongest event. Run it, change the end date by a day, run it again, and five of those six answers were already on your screen a moment ago.&lt;/p&gt;

&lt;p&gt;So the client wants a cache. The awkward part is not writing one. It is that there is no single right one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At a terminal the process dies after every command, so anything kept in memory is gone before it is useful. The cache has to be files on disk.&lt;/li&gt;
&lt;li&gt;In a notebook or a web service the process stays alive, and a dictionary in memory is both faster and simpler than touching the disk.&lt;/li&gt;
&lt;li&gt;On a group of machines that share work, neither of those is right, and the answer is redis or memcached, which this project has no business depending on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The usual way to offer a choice like that on a command line is a flag with a few allowed words, &lt;code&gt;--cache-type={none,memory,disk}&lt;/code&gt;, plus &lt;code&gt;--cache-path&lt;/code&gt; and &lt;code&gt;--cache-ttl&lt;/code&gt; that mean something for one of those words and nothing for the others, plus a small block of &lt;code&gt;if&lt;/code&gt; and &lt;code&gt;elif&lt;/code&gt; inside the program that turns the word into an object. And when someone wants the redis one, they cannot have it without editing your tool.&lt;/p&gt;

&lt;p&gt;That block is a copy of something already written down, in the same way that the completion script in &lt;a href="https://dev.to/mauvilsa/tab-completion-without-writing-a-completion-script-4nab"&gt;part 3&lt;/a&gt; and the &lt;code&gt;os.environ.get&lt;/code&gt; calls in &lt;a href="https://dev.to/mauvilsa/settings-from-the-environment-3kb0"&gt;part 4&lt;/a&gt; were copies. What is already written down, this time, is a type hint:&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;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Cache&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A collaborator, in plain Python
&lt;/h2&gt;

&lt;p&gt;Nothing below is about command lines. It is the code you would write anyway, in &lt;a href="https://github.com/mauvilsa/blog-earthquake-cli/blob/part-5/quakes_client.py" rel="noopener noreferrer"&gt;quakes_client.py&lt;/a&gt;, if the client were only ever used from Python.&lt;/p&gt;

&lt;p&gt;A base class that says what a cache has to be able to do:&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;class&lt;/span&gt; &lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Store for responses that were already received from the catalog.

    Subclasses decide where the entries are kept and for how long. The client
    only asks for a key and hands back whatever the catalog answered.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="nd"&gt;@abstractmethod&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;key&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;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return the entry kept for a key, or None if there is no valid one.

        Args:
            key: Identifier of the entry, the URL of the request.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="nd"&gt;@abstractmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set&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;key&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="n"&gt;value&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="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;Keep an entry for a key.

        Args:
            key: Identifier of the entry, the URL of the request.
            value: The response to keep.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And two implementations of it, one for each of the first two situations above:&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;class&lt;/span&gt; &lt;span class="nc"&gt;MemoryCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Cache that keeps entries in memory, for as long as the process lives.&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;max_entries&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;128&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Initialize the cache.

        Args:
            max_entries: How many entries to keep. The oldest one is dropped first.
        &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;max_entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_entries&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;_entries&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="nb"&gt;str&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;=&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;key&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;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&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="k"&gt;return&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;_entries&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="n"&gt;key&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;set&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;key&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="n"&gt;value&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="bp"&gt;None&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;len&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;_entries&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;del&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;_entries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;iter&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;_entries&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;_entries&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DiskCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Cache that keeps entries as files, so that they outlive the process.&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;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;~/.cache/quakes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ttl&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;3600.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;Initialize the cache.

        Args:
            path: Directory in which the entries are written.
            ttl: Seconds an entry stays valid before the catalog is asked again.
        &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;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expanduser&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;ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ttl&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;key&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;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&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;entry&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;_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_mtime&lt;/span&gt; &lt;span class="o"&gt;&amp;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;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&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;set&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;key&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="n"&gt;value&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="bp"&gt;None&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="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&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;span class="n"&gt;exist_ok&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;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&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;value&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;_entry&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;key&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;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Path&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two have different parameters, which is the whole point of the example. &lt;code&gt;max_entries&lt;/code&gt; only makes sense for a cache in memory. &lt;code&gt;path&lt;/code&gt; and &lt;code&gt;ttl&lt;/code&gt; only make sense for one on disk.&lt;/p&gt;

&lt;p&gt;The client takes one, as a new constructor parameter next to the ones 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="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;distance_unit&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;km&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;mi&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;km&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Cache&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="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Initialize the client.

        Args:
            distance_unit: Unit used for all distances, both given and returned.
            min_magnitude: Magnitude below which events are ignored by default.
            timeout: Seconds to wait for a response before giving up.
            cache: Where to keep the responses already received, if anywhere.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and consults it in the one private method that performs a 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="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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;USGS_URL&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;if&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;cache&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="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kept&lt;/span&gt; &lt;span class="p"&gt;:&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;cache&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="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;kept&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&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="nc"&gt;Request&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="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&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;except&lt;/span&gt; &lt;span class="n"&gt;HTTPError&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;reported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ex&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="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;CatalogError&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reported&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;if&lt;/span&gt; &lt;span class="n"&gt;reported&lt;/span&gt; &lt;span class="k"&gt;else&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;reason&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (requesting &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="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;
        &lt;span class="k"&gt;if&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;cache&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&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;data&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what the client does &lt;em&gt;not&lt;/em&gt; do. It does not choose a cache, it does not build one, and it never names &lt;code&gt;MemoryCache&lt;/code&gt; or &lt;code&gt;DiskCache&lt;/code&gt;. It says what it needs, as a type, and waits to be given one:&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;quakes_client&lt;/span&gt; &lt;span class="kn"&gt;import&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;MemoryCache&lt;/span&gt;

&lt;span class="n"&gt;catalog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EarthquakeCatalog&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="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;MemoryCache&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 dependency injection, and there is nothing clever about it. It is ordinary Python, it is what makes the class easy to test, and every part of it is useful with no command line involved. The rest of this post is about what jsonargparse does with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  An option nobody added
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;quakes_cli.py&lt;/code&gt; was not touched. The help has one more option:&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;Client for the earthquake catalog of the U.S. Geological Survey:
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;  ARG:   --cache.help [CLASS_PATH_OR_NAME]
                        Show the help for the given subclass of Cache and
                        exit.
  ARG:   --cache CACHE
  ENV:   QUAKES_CACHE
                        Where to keep the responses already received, if
                        anywhere. (type: Cache | null, default: null, known
                        subclasses: quakes_client.MemoryCache,
                        quakes_client.DiskCache)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;known subclasses&lt;/code&gt; is the list nobody had to write. jsonargparse looks for subclasses of &lt;code&gt;Cache&lt;/code&gt; in the modules the program has imported, and those are what it found. Adding a third one to the client adds a third name to that line.&lt;/p&gt;

&lt;p&gt;This is a different treatment from the one &lt;code&gt;Area&lt;/code&gt; got in &lt;a href="https://dev.to/mauvilsa/from-api-client-to-cli-without-writing-a-parser-3h01"&gt;part 1&lt;/a&gt;, and the difference comes from the classes, not from any setting. &lt;code&gt;Area&lt;/code&gt; is a concrete dataclass, so there is nothing to decide and its three fields became options directly: &lt;code&gt;--area.latitude&lt;/code&gt; and friends. &lt;code&gt;Cache&lt;/code&gt; is a base class with more than one implementation, so there &lt;em&gt;is&lt;/em&gt; something to decide, and the decision comes first. Which options exist depends on what you decided.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing one
&lt;/h2&gt;

&lt;p&gt;The value of &lt;code&gt;--cache&lt;/code&gt; is a class:&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;--cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DiskCache summary &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;--end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-08-01
&lt;span class="go"&gt;{
  "start": "2026-07-01",
  "end": "2026-08-01",
  "total": 3817,
  "by_magnitude": {
    "minor 2.0-3.9": 2330,
    "light 4.0-4.9": 1262,
    "moderate 5.0-5.9": 214,
    "strong 6.0-6.9": 10,
    "major 7.0+": 1
  },
  "strongest": {
    "time": "2026-07-17 14:48:40.227000+00:00",
    "magnitude": 7.3,
    "depth": 22,
    "latitude": 14.6361,
    "longitude": -92.8969,
    "id": "us7000t1bu",
    "place": "52 km W of Puerto Madero, Mexico"
  }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it a second time and the six requests are not made:&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;--cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DiskCache summary &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;--end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-08-01 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="go"&gt;real    0m0.670s
&lt;/span&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;--cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DiskCache summary &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;--end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-08-01 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="go"&gt;real    0m0.237s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most of that second number is Python starting up and the parser being built. The catalog was not contacted at all.&lt;/p&gt;

&lt;p&gt;The parser imported the class, built it with its defaults, and passed the instance to &lt;code&gt;EarthquakeCatalog.__init__&lt;/code&gt;. That is the injection, performed by the command line instead of by a line of Python.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DiskCache&lt;/code&gt; is the short name and works because the class is one of the known subclasses above. The full import path, &lt;code&gt;quakes_client.DiskCache&lt;/code&gt;, always works and is what a config file or a script should use, since it cannot become ambiguous later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The options come from the class you chose
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--cache.help&lt;/code&gt; prints the help of an implementation, built from that implementation's own signature and docstring, 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;quakes &lt;span class="nt"&gt;--cache&lt;/span&gt;.help DiskCache
&lt;span class="go"&gt;usage: quakes [--cache.path PATH] [--cache.ttl TTL]

Help for --cache.help=quakes_client.DiskCache

Cache that keeps entries as files, so that they outlive the process:
  --cache.path PATH  Directory in which the entries are written. (type: &amp;lt;class
&lt;/span&gt;&lt;span class="gp"&gt;                     'Path'&amp;gt;&lt;/span&gt;, default: ~/.cache/quakes&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;  --cache.ttl TTL    Seconds an entry stays valid before the catalog is asked
                     again. (type: float, default: 3600.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 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;--cache&lt;/span&gt;.help MemoryCache
&lt;span class="go"&gt;usage: quakes [--cache.max_entries MAX_ENTRIES]

Help for --cache.help=quakes_client.MemoryCache

Cache that keeps entries in memory, for as long as the process lives:
  --cache.max_entries MAX_ENTRIES
                        How many entries to keep. The oldest one is dropped
                        first. (type: int, default: 128)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two implementations, two different help pages, no shared list of flags between them. And they are set the way you would expect:&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;--cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DiskCache &lt;span class="nt"&gt;--cache&lt;/span&gt;.ttl&lt;span class="o"&gt;=&lt;/span&gt;600 &lt;span class="nt"&gt;--cache&lt;/span&gt;.path&lt;span class="o"&gt;=&lt;/span&gt;/tmp/quakes &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;      count --start=2026-07-01 --min_magnitude=6
20
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two small rules go with that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The class comes before its options.&lt;/strong&gt; The parser has to know which class it is building before it can decide what &lt;code&gt;--cache.ttl&lt;/code&gt; means, so the reverse order is an error:&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;--cache&lt;/span&gt;.ttl&lt;span class="o"&gt;=&lt;/span&gt;600 &lt;span class="nt"&gt;--cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DiskCache count
&lt;span class="go"&gt;error: Parser key "cache":
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;    - Expected an instantiatable class, but quakes_client.Cache is abstract
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;An option that the chosen class does not have is an error, not a value that is ignored:&lt;/strong&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 &lt;span class="nt"&gt;--cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;MemoryCache &lt;span class="nt"&gt;--cache&lt;/span&gt;.ttl&lt;span class="o"&gt;=&lt;/span&gt;600 count
&lt;span class="go"&gt;error: Parser key "cache":
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;    - Problem with given class_path 'quakes_client.MemoryCache':
        Option 'ttl' is not accepted
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is the behaviour you want and the one a hand written &lt;code&gt;--cache-type&lt;/code&gt; flag almost never has. There, &lt;code&gt;--cache-ttl&lt;/code&gt; next to &lt;code&gt;--cache-type=memory&lt;/code&gt; is usually accepted and quietly does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A class the tool has never heard of
&lt;/h2&gt;

&lt;p&gt;Here is the part that a fixed list of words cannot do.&lt;/p&gt;

&lt;p&gt;Say you want to know which requests are being served from the cache and which are not. That is a cache of your own, and it is short, because it only changes one of the two methods:&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="c1"&gt;# logging_cache.py
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A cache of my own, in a file the quakes command knows nothing about.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;quakes_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DiskCache&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoggingCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DiskCache&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Disk cache that reports on standard error whether each key was found.&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;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;key&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;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;super&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="n"&gt;key&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hit &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;miss&lt;/span&gt;&lt;span class="sh"&gt;'&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;key&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="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put that file anywhere the Python process can import it from, and name 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;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; quakes &lt;span class="nt"&gt;--cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;logging_cache.LoggingCache &lt;span class="nt"&gt;--cache&lt;/span&gt;.ttl&lt;span class="o"&gt;=&lt;/span&gt;600 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;      count --start=2026-07-01 --min_magnitude=6
cache miss: https://earthquake.usgs.gov/fdsnws/event/1/count?format=geojson&amp;amp;starttime=2026-07-01&amp;amp;minmagnitude=6.0
20
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; quakes &lt;span class="nt"&gt;--cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;logging_cache.LoggingCache &lt;span class="nt"&gt;--cache&lt;/span&gt;.ttl&lt;span class="o"&gt;=&lt;/span&gt;600 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;      count --start=2026-07-01 --min_magnitude=6
cache hit : https://earthquake.usgs.gov/fdsnws/event/1/count?format=geojson&amp;amp;starttime=2026-07-01&amp;amp;minmagnitude=6.0
20
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--cache.ttl&lt;/code&gt; still works, because &lt;code&gt;LoggingCache&lt;/code&gt; inherits the constructor of &lt;code&gt;DiskCache&lt;/code&gt;, and the help page of a class written five minutes ago is as complete as the ones that ship with the tool:&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="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; quakes &lt;span class="nt"&gt;--cache&lt;/span&gt;.help logging_cache.LoggingCache
&lt;span class="go"&gt;usage: quakes [--cache.path PATH] [--cache.ttl TTL]

Help for --cache.help=logging_cache.LoggingCache

Disk cache that reports on standard error whether each key was found:
  --cache.path PATH  Directory in which the entries are written. (type: &amp;lt;class
&lt;/span&gt;&lt;span class="gp"&gt;                     'Path'&amp;gt;&lt;/span&gt;, default: ~/.cache/quakes&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;  --cache.ttl TTL    Seconds an entry stays valid before the catalog is asked
                     again. (type: float, default: 3600.0)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;quakes&lt;/code&gt; was not reinstalled, not edited, and not told that this class exists. There is no plug-in registry, no entry point group, no &lt;code&gt;--load-plugin&lt;/code&gt; argument. The redis cache from the beginning of this post is now somebody else's file, in somebody else's package, and it costs this project nothing.&lt;/p&gt;

&lt;p&gt;That is the property worth naming, because it is what the fixed list of words cannot give you. A tool whose choices are &lt;code&gt;{none,memory,disk}&lt;/code&gt; is a tool whose author has to say yes before you can have a fourth. A tool whose choice is a type hint is one where the author is not involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is checked, and what is not
&lt;/h2&gt;

&lt;p&gt;Two conditions have to hold, and both produce a clear failure at startup rather than a strange one later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The class has to be importable by the process.&lt;/strong&gt; Nothing is searched for; the dotted path is imported the way Python imports anything. &lt;code&gt;PYTHONPATH=.&lt;/code&gt; above is what makes a file in the current directory reachable, since the &lt;code&gt;quakes&lt;/code&gt; command is a script installed elsewhere and does not add your working directory to &lt;code&gt;sys.path&lt;/code&gt;. If the class lives in an installed package, nothing extra is needed.&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;--cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;logging_cache.LoggingCache count
&lt;span class="go"&gt;error: Parser key "cache":
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;    - Problem with given class_path 'logging_cache.LoggingCache':
        No module named 'logging_cache'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The class has to satisfy the hint.&lt;/strong&gt; Any importable name is accepted as text; only a subclass of &lt;code&gt;Cache&lt;/code&gt; survives:&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;--cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;quakes_client.Area count
&lt;span class="go"&gt;error: Parser key "cache":
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;    - Import path does not correspond to a subclass of Cache
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the base class itself is not a choice, because it is abstract and cannot be built:&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;--cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Cache count
&lt;span class="go"&gt;error: Parser key "cache":
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;    - Expected an instantiatable class, but is abstract
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is worth being explicit about what all of this means: the command line can now name a class, and the program will import the module that contains it. Importing a Python module runs its top level code. In practice this is the same trust boundary a command line already has — anyone who can choose your arguments can usually choose your &lt;code&gt;PYTHONPATH&lt;/code&gt;, and on that machine they can just run Python — but it is a real widening of what an argument can do, and it deserves a thought before a tool that takes arguments from somewhere less trusted than a person's keyboard.&lt;/p&gt;

&lt;p&gt;If you want a fixed set instead, say so in the hint. Write the parameter as a union of concrete classes rather than as the base class:&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;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MemoryCache&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;DiskCache&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and those two, plus anything derived from them, are all that is accepted. A different subclass of &lt;code&gt;Cache&lt;/code&gt; is not:&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;--cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;other_cache.NullCache count
&lt;span class="go"&gt;error: Parser key "cache":
  Does not validate against any of the Union subtypes
&lt;/span&gt;&lt;span class="gp"&gt;  Subtypes: [&amp;lt;class 'quakes_client.MemoryCache'&amp;gt;&lt;/span&gt;, &amp;lt;class &lt;span class="s1"&gt;'quakes_client.DiskCache'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;, &amp;lt;class &lt;span class="s1"&gt;'NoneType'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;]&lt;/span&gt;
&lt;span class="go"&gt;  Errors:
    - Import path does not correspond to a subclass of MemoryCache
    - Import path does not correspond to a subclass of DiskCache
&lt;/span&gt;&lt;span class="c"&gt;    ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is the same trade as everywhere else in this series. The command line is as open, or as closed, as the type hint says it is, and the type hint is in the client where a reader of the code will see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other four posts still apply
&lt;/h2&gt;

&lt;p&gt;Nothing about &lt;code&gt;--cache&lt;/code&gt; is a special case, so everything the earlier posts added works on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A config file&lt;/strong&gt; (&lt;a href="https://dev.to/mauvilsa/a-cli-that-works-from-anywhere-5hf0"&gt;part 2&lt;/a&gt;) writes the choice as a class and its arguments. This is the full form of the value, and it is what the dotted options on the command line are a shorthand for:&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;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;class_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;quakes_client.DiskCache&lt;/span&gt;
  &lt;span class="na"&gt;init_args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;21600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two layers combine per option rather than per value, so the command line can change one setting of the cache without repeating which cache it is:&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;$ quakes --config config.yaml --cache.ttl=60 --print_config count&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;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;class_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;quakes_client.DiskCache&lt;/span&gt;
  &lt;span class="na"&gt;init_args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.cache/quakes&lt;/span&gt;
    &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60.0&lt;/span&gt;
&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;--cache=null&lt;/code&gt; turns it off for one run, whatever the file said.&lt;/p&gt;

&lt;p&gt;If all you want is the class with its own defaults, the file can also say just the name, exactly as the command line does:&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="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MemoryCache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Environment variables&lt;/strong&gt; (&lt;a href="https://dev.to/mauvilsa/settings-from-the-environment-3kb0"&gt;part 4&lt;/a&gt;) take the same two forms, and this is a case where the JSON one is bearable, because it is one small object:&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="nv"&gt;QUAKES_CACHE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DiskCache 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="go"&gt;3979
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;QUAKES_CACHE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"class_path": "quakes_client.DiskCache", "init_args": {"ttl": 60}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;      quakes count --start=2026-07-01
3979
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Completion&lt;/strong&gt; (&lt;a href="https://dev.to/mauvilsa/tab-completion-without-writing-a-completion-script-4nab"&gt;part 3&lt;/a&gt;) offers the implementations, since the parser knows them:&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;--cache&lt;/span&gt; &amp;lt;TAB&amp;gt;&amp;lt;TAB&amp;gt;
&lt;span class="gp"&gt;Expected type: Cache | null;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;3/3 matched choices
&lt;span class="go"&gt;null                       quakes_client.MemoryCache
quakes_client.DiskCache
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and it offers the options of every known implementation, telling you which one each belongs to, since it cannot know yet what you are about to choose:&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;--cache&lt;/span&gt;.&amp;lt;TAB&amp;gt;&amp;lt;TAB&amp;gt;
&lt;span class="go"&gt;--cache.help         --cache.path
--cache.max_entries  --cache.ttl

&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;--cache&lt;/span&gt;.ttl &amp;lt;TAB&amp;gt;&amp;lt;TAB&amp;gt;
&lt;span class="gp"&gt;Expected type: float;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Accepted by subclasses: DiskCache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;--print_config&lt;/code&gt;&lt;/strong&gt; is the one to reach for when a run does not behave as expected, because it prints the finished recipe: which class, with which arguments, after the file, the environment and the command line have all had their turn. It is also the easiest way to write the config file above — run the command once with the options you want, and paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same hint in other shapes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Cache | None&lt;/code&gt; is one arrangement. A few others behave the way you would guess, and are worth knowing exist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A parameter typed &lt;code&gt;list[Cache]&lt;/code&gt; takes several of them. Each &lt;code&gt;+=&lt;/code&gt; appends one more, and the dotted options that follow apply to the one most recently appended.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Callable[[], Cache]&lt;/code&gt; asks for a factory instead of a finished object, for when one shared cache is wrong and multiple are needed: one per thread, one per request, one per retry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The factory is the one worth showing, because nothing about the command line changes. Written this way:&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;new_cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[],&lt;/span&gt; &lt;span class="n"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MemoryCache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the option offers the same implementations, and its value is still a class with its arguments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  --new_cache NEW_CACHE
                        Builds a fresh cache each time it is called. (type:
                        Callable[[], Cache], default: {'class_path':
                        'quakes_client.MemoryCache'}, known subclasses:
                        quakes_client.MemoryCache, quakes_client.DiskCache)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;--new_cache=DiskCache --new_cache.ttl=60&lt;/code&gt; is typed exactly as it was before. What the constructor receives is not a &lt;code&gt;DiskCache&lt;/code&gt;, though. It is something it can call, with the choice and the arguments already decided:&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;functools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="n"&gt;default_class_instantiator&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="nc"&gt;quakes_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DiskCache&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;,
                  path=PosixPath(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="o"&gt;~/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;quakes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;), ttl=60.0)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every call to it builds a new cache. That splits the two decisions the way you want them: &lt;em&gt;what&lt;/em&gt; to build is chosen where the command is typed, and &lt;em&gt;when&lt;/em&gt; to build it stays in the code that knows how many are needed and when they stop being valid.&lt;/p&gt;

&lt;p&gt;If the callable's own signature has parameters, they are taken off the command line, because the caller is going to supply them. With &lt;code&gt;Callable&lt;/code&gt; it is their number that counts: &lt;code&gt;Callable[[int, str], Cache]&lt;/code&gt; reserves the first two parameters of whichever class was chosen, whatever they are called, and &lt;code&gt;--new_cache.help&lt;/code&gt; then offers only the rest. A callable &lt;code&gt;Protocol&lt;/code&gt;, a class with a &lt;code&gt;__call__&lt;/code&gt; method whose signature is the one you want, reserves them by name instead, so the ones left for the caller do not have to be the leading ones.&lt;/p&gt;

&lt;p&gt;The shape this post is about, though, is the common one, and it appears far away from caches. An optimizer in a training script, an authentication method, a storage backend, a notifier, an exporter: anything a program should be able to swap without being rewritten is a parameter whose type is a base class. It is how &lt;a href="https://lightning.ai/docs/pytorch/stable/cli/lightning_cli.html" rel="noopener noreferrer"&gt;Lightning&lt;/a&gt;, which builds its CLI on jsonargparse, lets &lt;code&gt;--optimizer=torch.optim.AdamW --optimizer.lr=0.001&lt;/code&gt; reach a training run that does not know AdamW exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the other libraries do here
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/mauvilsa/from-api-client-to-cli-without-writing-a-parser-3h01"&gt;Part 1&lt;/a&gt; compared the alternatives and pinned the versions it checked, so here is the same exercise for this one feature, against click 8.4, typer 0.27, fire 0.7 and tyro 1.0. The example is a shortened &lt;code&gt;Catalog&lt;/code&gt; class with the same &lt;code&gt;cache: Cache | None = None&lt;/code&gt; parameter and the same two implementations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/google/python-fire" rel="noopener noreferrer"&gt;Fire&lt;/a&gt;&lt;/strong&gt; accepts the argument and passes the text through untouched. &lt;code&gt;--cache=DiskCache&lt;/code&gt; arrives at the constructor as the string &lt;code&gt;'DiskCache'&lt;/code&gt;, and &lt;code&gt;--cache='{"class_path": "DiskCache"}'&lt;/code&gt; arrives as a plain &lt;code&gt;dict&lt;/code&gt;. Nothing is imported, nothing is built, nothing is checked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/fastapi/typer" rel="noopener noreferrer"&gt;Typer&lt;/a&gt;&lt;/strong&gt; stops before the program starts, in the same way part 1 found it stopping on a dataclass: &lt;code&gt;RuntimeError: Type not yet supported: &amp;lt;class 'catalog.Cache'&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/pallets/click" rel="noopener noreferrer"&gt;Click&lt;/a&gt;&lt;/strong&gt; infers nothing by design, so this is a thing you write: a &lt;code&gt;click.Choice&lt;/code&gt; of names, a mapping from name to class, and one option per constructor argument of every implementation. Which is exactly the hand written version this post started from. Click will not stop you from doing it well, but it is your code, and it grows with each implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/brentyi/tyro" rel="noopener noreferrer"&gt;Tyro&lt;/a&gt;&lt;/strong&gt; is the one with a real answer. Written as a union of concrete classes, the choice becomes a subcommand, tyro builds that class from the arguments that follow it, and the program runs with the instance. The subcommands cost a few lines, because tyro builds a CLI from a function or a class rather than from a class's methods. An unbound method is a function whose first parameter is &lt;code&gt;self&lt;/code&gt;, so annotating it with the client class is enough for tyro to build the client and pass it in:&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;EarthquakeCatalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__annotations__&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache&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;Union&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MemoryCache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DiskCache&lt;/span&gt;&lt;span class="p"&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;subcommands&lt;/span&gt; &lt;span class="o"&gt;=&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getmembers&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;inspect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isfunction&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&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="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__annotations__&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;self&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;Annotated&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;tyro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;(&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;client&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="n"&gt;subcommands&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tyro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subcommand_cli_from_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subcommands&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All five methods arrive with their own parameters and their docstrings, and nothing is restated. The equivalent of the command earlier in this post:&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;--cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DiskCache &lt;span class="nt"&gt;--cache&lt;/span&gt;.ttl&lt;span class="o"&gt;=&lt;/span&gt;60 summary &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;--end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-08-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is 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_tyro.py summary &lt;span class="nt"&gt;--start&lt;/span&gt; 2026-07-01 &lt;span class="nt"&gt;--end&lt;/span&gt; 2026-08-01 &lt;span class="se"&gt;\&lt;/span&gt;
    client.cache:disk-cache &lt;span class="nt"&gt;--client&lt;/span&gt;.cache.ttl 60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is the type hint, and the type hint lives in the client. &lt;code&gt;EarthquakeCatalog&lt;/code&gt; declares &lt;code&gt;cache: Cache | None&lt;/code&gt;, which is what the author of a client writes when no command line is in the picture. tyro reads it as naming the &lt;code&gt;Cache&lt;/code&gt; class itself, which is abstract, so there is nothing to build: the option becomes &lt;code&gt;--client.cache {fixed}  (fixed to: None)&lt;/code&gt;, and asking for &lt;code&gt;client.cache:disk-cache&lt;/code&gt; is an unrecognized option. To get a choice, the parameter has to be rewritten as &lt;code&gt;MemoryCache | DiskCache | None&lt;/code&gt; — an edit to &lt;a href="https://github.com/mauvilsa/blog-earthquake-cli/blob/main/quakes_client.py" rel="noopener noreferrer"&gt;quakes_client.py&lt;/a&gt; made for the benefit of the command line, and repeated every time an implementation is added. The snippet above makes that edit from the outside, so that the client in the repository stays the one the other posts describe.&lt;/p&gt;

&lt;p&gt;Closed sets are a good design, and the union above says exactly that in jsonargparse too. The point is which way the dependency runs. What a closed set cannot accept is the name of a class written by a user of your tool, in a module your tool has never imported.&lt;/p&gt;

&lt;p&gt;Neither of the other two shapes from earlier is available. A &lt;code&gt;list[DiskCache]&lt;/code&gt; produces no option at all, the parameter simply not appearing in the help. And an instance factory, &lt;code&gt;Callable[[], DiskCache]&lt;/code&gt;, is fixed to whatever its default is and refuses any value given for it.&lt;/p&gt;

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

&lt;p&gt;Five posts, and the client has still not imported jsonargparse. What it gained this time was a collaborator it does not construct, described by a base class and a type hint, which is the shape a testable class has anyway.&lt;/p&gt;

&lt;p&gt;Out of that, without a line in &lt;a href="https://github.com/mauvilsa/blog-earthquake-cli/blob/main/quakes_cli.py" rel="noopener noreferrer"&gt;quakes_cli.py&lt;/a&gt;: an option that takes an implementation, a help page per implementation built from its own docstring, its constructor arguments as options, all of it settable from a config file or the environment, completed at the prompt, validated before anything runs, and open to classes that neither the tool nor its author has ever seen.&lt;/p&gt;

&lt;p&gt;The trade is the one the series keeps making, and this is the largest version of it. A plug-in system is normally a subproject: a registry, a way to name plug-ins, a way to load them, a way to configure them, documentation for all of that. Here it is a parameter, typed honestly, in a class that does not know it has users at a terminal.&lt;/p&gt;

</description>
      <category>python</category>
      <category>cli</category>
      <category>architecture</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>Settings from the environment</title>
      <dc:creator>Mauricio Villegas</dc:creator>
      <pubDate>Wed, 02 Sep 2026 05:53:36 +0000</pubDate>
      <link>https://dev.to/mauvilsa/settings-from-the-environment-3kb0</link>
      <guid>https://dev.to/mauvilsa/settings-from-the-environment-3kb0</guid>
      <description>&lt;p&gt;The &lt;a href="https://dev.to/mauvilsa/a-cli-that-works-from-anywhere-5hf0"&gt;second post&lt;/a&gt; gave the tool a config file it finds on its own, so that &lt;code&gt;~/.config/quakes.yaml&lt;/code&gt; decides what &lt;code&gt;quakes&lt;/code&gt; does from any directory. That solves the problem for a person with a home directory, and it is the right default for one. It also assumes a filesystem you can write to before the program runs, which is the assumption that fails everywhere else the command is likely to end up.&lt;/p&gt;

&lt;p&gt;A container image is built once and run with different settings. A CI job gets its configuration from the thing that started it. A systemd unit has &lt;code&gt;Environment=&lt;/code&gt; lines and no obvious place to put a YAML file. In all three the settings arrive as environment variables, because that is the one channel a process always has, and the usual response is a block of &lt;code&gt;os.environ.get&lt;/code&gt; calls somewhere near the top of the program, with its own names, its own defaults and its own idea of how to turn &lt;code&gt;"6"&lt;/code&gt; into a float.&lt;/p&gt;

&lt;p&gt;That block is another copy of the interface, in the same way the completion script in &lt;a href="https://dev.to/mauvilsa/tab-completion-without-writing-a-completion-script-4nab"&gt;part 3&lt;/a&gt; was a copy. The parser already knows every setting the program has, what type each one is and what it defaults to. Reading them from the environment instead of from &lt;code&gt;argv&lt;/code&gt; is a different source for the same values, and it is two keyword arguments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two arguments, again
&lt;/h2&gt;

&lt;p&gt;The change is in &lt;a href="https://github.com/mauvilsa/blog-earthquake-cli/blob/6f086e9094a38537c989c22769de824c95b56080/quakes_cli.py#L74-L78" rel="noopener noreferrer"&gt;quakes_cli.py&lt;/a&gt;, next to the one from part 2:&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;result&lt;/span&gt; &lt;span class="o"&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="c1"&gt;# Settings written here apply from any directory, without --config.
&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="c1"&gt;# And these let a process pass settings in without a file at all.
&lt;/span&gt;    &lt;span class="c1"&gt;# The prefix is spelled out because prog changes when the module is
&lt;/span&gt;    &lt;span class="c1"&gt;# run directly, and the variable names should not change with it.
&lt;/span&gt;    &lt;span class="n"&gt;env_prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;QUAKES&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;default_env&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;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;default_env=True&lt;/code&gt; is what turns the parsing on. &lt;code&gt;env_prefix&lt;/code&gt; is the first part of every variable name, and it is worth being explicit about even here, where the value looks redundant. Left unset, the prefix is derived from the program name, which is &lt;code&gt;quakes&lt;/code&gt; when the installed command runs and &lt;code&gt;quakes_cli.py&lt;/code&gt; when the module is run directly, as &lt;a href="https://dev.to/mauvilsa/from-api-client-to-cli-without-writing-a-parser-3h01"&gt;part 1&lt;/a&gt; still does. Deriving it would mean &lt;code&gt;QUAKES_MIN_MAGNITUDE&lt;/code&gt; and &lt;code&gt;QUAKES_CLI_MIN_MAGNITUDE&lt;/code&gt; naming the same setting depending on how the program was started. Writing the prefix down costs one line and removes that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The names are already decided
&lt;/h2&gt;

&lt;p&gt;The rule is &lt;code&gt;PREFIX_&lt;/code&gt;, then the argument name in upper case, with each dot replaced by two underscores. Client options are one level deep, so &lt;code&gt;--min_magnitude&lt;/code&gt; is &lt;code&gt;QUAKES_MIN_MAGNITUDE&lt;/code&gt;. A subcommand's options are two, so &lt;code&gt;search&lt;/code&gt;'s &lt;code&gt;--limit&lt;/code&gt; is &lt;code&gt;QUAKES_SEARCH__LIMIT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Which is a rule nobody should have to remember, and does not have to, because the help now prints the variable next to the option it sets:&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:
  ARG:   -h, --help     Show this help message and exit.
  ARG:   --config CONFIG
  ENV:   QUAKES_CONFIG
                        Path to a configuration file.
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;Client for the earthquake catalog of the U.S. Geological Survey:
  ARG:   --distance_unit {km,mi}
  ENV:   QUAKES_DISTANCE_UNIT
                        Unit used for all distances, both given and returned.
                        (type: Literal['km', 'mi'], default: km)
  ARG:   --min_magnitude MIN_MAGNITUDE
  ENV:   QUAKES_MIN_MAGNITUDE
                        Magnitude below which events are ignored by default.
                        (type: float, default: 2.5)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ARG:&lt;/code&gt; and &lt;code&gt;ENV:&lt;/code&gt; labels appear only once environment parsing is on; before that the help looks as it did in the earlier posts. This is the part that tends to be missing when the variables are read by hand. A program with fifteen &lt;code&gt;os.environ.get&lt;/code&gt; calls has fifteen names documented in a README if you are lucky, and the README is where they go out of date. Here the list cannot drift, for the same reason the completions could not: it is printed from the parser, and the parser was built from the client's signature.&lt;/p&gt;

&lt;p&gt;The same holds one level down, including for the positional:&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 event &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;span class="go"&gt;  ARG:   event_id
  ENV:   QUAKES_EVENT__EVENT_ID
                        Identifier of the event, e.g. ``us7000srb1``.
                        (required, type: str)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a run of the tool can be assembled entirely out of the environment:&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="nv"&gt;QUAKES_MIN_MAGNITUDE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6 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="go"&gt;13
&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;--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-07-01
&lt;span class="go"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The subcommand is a variable too
&lt;/h2&gt;

&lt;p&gt;Note the name in the help for the subcommands group:&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;subcommands:
  ENV:   QUAKES_SUBCOMMAND
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The choice of subcommand is an argument like the others, so it is available the same way, and with it the command line can be empty. That is exactly the shape a container wants, and it is what makes the whole &lt;a href="https://github.com/mauvilsa/blog-earthquake-cli/blob/main/Dockerfile" rel="noopener noreferrer"&gt;Dockerfile&lt;/a&gt; this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /src&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pyproject.toml README.md LICENSE quakes_cli.py quakes_client.py ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; .

&lt;span class="c"&gt;# No arguments. Everything the run needs is passed with -e, including which&lt;/span&gt;
&lt;span class="c"&gt;# subcommand to run.&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["quakes"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;CMD&lt;/code&gt;, and nothing in the image that says what it does when it starts. Build it once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; quakes &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A real image is named &lt;code&gt;{registry}/{project}/quakes-cli:{tag}&lt;/code&gt;, and the short name is only used here to keep the runs below down to the part that matters. It does mean the image is spelled exactly like the command, so in every &lt;code&gt;docker run&lt;/code&gt; that follows, the bare &lt;code&gt;quakes&lt;/code&gt; on the last line is the image and anything after it is what gets appended to the entry point.&lt;/p&gt;

&lt;p&gt;With that, the run is described entirely by its environment:&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;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    -e QUAKES_SUBCOMMAND=count \
    -e QUAKES_COUNT__MIN_MAGNITUDE=6 \
    -e QUAKES_COUNT__START=2026-07-01 \
    quakes
13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no home directory in that container, therefore no &lt;code&gt;~/.config/quakes.yaml&lt;/code&gt;, therefore nothing but what was handed in. The same image and the same entry point, with a different set of variables, give a different program:&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;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    -e QUAKES_SUBCOMMAND=search \
    -e QUAKES_DISTANCE_UNIT=mi \
    -e QUAKES_SEARCH__MIN_MAGNITUDE=6.5 \
    -e QUAKES_SEARCH__START=2026-07-01 \
    -e QUAKES_SEARCH__LIMIT=3 \
    quakes
time              magnitude  depth  latitude  longitude  id          place
2026-07-28 07:27  6.8        6.21   32.6817   130.7217   us6000tgb9  The 2026 Kumamoto Region, Japan Earthquake
2026-07-17 14:48  7.3        13.67  14.6361   -92.8969   us7000t1bu  52 km W of Puerto Madero, Mexico
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dates arrived as dates and &lt;code&gt;mi&lt;/code&gt; reached the client, because &lt;code&gt;register_type(date, ...)&lt;/code&gt; and the &lt;code&gt;Literal&lt;/code&gt; from part 1 apply here too. The environment is a different source, not a different type system.&lt;/p&gt;

&lt;p&gt;None of this closes the command line off. An &lt;code&gt;ENTRYPOINT&lt;/code&gt; with no &lt;code&gt;CMD&lt;/code&gt; appends whatever you put after the image name, so the same image is still the tool it was:&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;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; quakes count &lt;span class="nt"&gt;--min_magnitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6 &lt;span class="nt"&gt;--start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-07-01
&lt;span class="go"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is the property worth protecting when a program grows a container. One image, driven by variables in production and by arguments when you are trying to work out what it did, with a single description of the settings behind both. A &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;, a Kubernetes &lt;code&gt;env:&lt;/code&gt; block and a systemd unit's &lt;code&gt;Environment=&lt;/code&gt; lines all reduce to the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this sits in the order
&lt;/h2&gt;

&lt;p&gt;There are now five places a value can come from, and the order between them is fixed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The default in the client's signature.&lt;/li&gt;
&lt;li&gt;The files in &lt;code&gt;default_config_files&lt;/code&gt;, in the order given.&lt;/li&gt;
&lt;li&gt;The config file named by &lt;code&gt;QUAKES_CONFIG&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The individual variables, &lt;code&gt;QUAKES_MIN_MAGNITUDE&lt;/code&gt; and friends.&lt;/li&gt;
&lt;li&gt;The command line, left to right.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With part 2's file in place:&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;each layer overrides the one before 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;$&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="go"&gt;1486
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;QUAKES_MIN_MAGNITUDE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6 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="go"&gt;13
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;QUAKES_MIN_MAGNITUDE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6 quakes &lt;span class="nt"&gt;--min_magnitude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7 count &lt;span class="nt"&gt;--start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-07-01
&lt;span class="go"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ordering is the one that makes deployment work: the file is what the machine believes, the environment is what this run believes, the command line is what you believe right now. A container inherits the image's defaults and overrides the two settings the job cares about, without a file being rewritten anywhere.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;QUAKES_CONFIG&lt;/code&gt; deserves its own line in that list. It takes a path and loads a whole file, which is the answer when a run needs more than a couple of settings changed, and it sits &lt;em&gt;below&lt;/em&gt; the individual variables rather than above:&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;# ci.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;5.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 console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;QUAKES_CONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ci.yaml 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="go"&gt;262
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;QUAKES_CONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ci.yaml &lt;span class="nv"&gt;QUAKES_MIN_MAGNITUDE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6 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="go"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is the combination a deployment usually ends up wanting, and in a container it is literally that: mount the file, name it with a variable, override the one setting this run disagrees about.&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;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/ci.yaml:/etc/quakes.yaml:ro"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    -e QUAKES_CONFIG=/etc/quakes.yaml \
    -e QUAKES_SUBCOMMAND=count -e QUAKES_COUNT__START=2026-07-01 \
    quakes
262
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/ci.yaml:/etc/quakes.yaml:ro"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    -e QUAKES_CONFIG=/etc/quakes.yaml -e QUAKES_MIN_MAGNITUDE=6 \
    -e QUAKES_SUBCOMMAND=count -e QUAKES_COUNT__START=2026-07-01 \
    quakes
13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing &lt;code&gt;--config /etc/quakes.yaml&lt;/code&gt; after the image name works too, and puts the file above the variables instead. A third option is to bake the file into the image and name it in &lt;code&gt;default_config_files&lt;/code&gt;, which makes it the image's defaults rather than the run's.&lt;/p&gt;

&lt;p&gt;When the layers stop being obvious, &lt;code&gt;--print_config&lt;/code&gt; from part 2 answers the question, and it answers it after the environment has been read:&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="nv"&gt;QUAKES_SUBCOMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;count &lt;span class="nv"&gt;QUAKES_MIN_MAGNITUDE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6 quakes &lt;span class="nt"&gt;--print_config&lt;/span&gt;
&lt;span class="go"&gt;distance_unit: mi
min_magnitude: 6.0
timeout: 30.0
count:
  start: null
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the debugging tool worth knowing about before you need it. "Which of these four things won" is the whole difficulty of layered configuration, and the command prints the answer rather than requiring you to reconstruct it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing is trusted more for arriving this way
&lt;/h2&gt;

&lt;p&gt;Values from the environment go through the same validation as everything else, which is not the usual behaviour of a hand written &lt;code&gt;os.environ.get&lt;/code&gt;, where the string reaches the code as a string and fails later, somewhere less helpful:&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;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;QUAKES_SUBCOMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;count &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;QUAKES_DISTANCE_UNIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;miles quakes
&lt;span class="go"&gt;error: Parser key "distance_unit":
  Expected a typing.Literal['km', 'mi']. Got value: miles
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="go"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same &lt;code&gt;Literal&lt;/code&gt; that produced the choices in the help in part 1 and the completions in part 3 rejects the value here, and it does so before the client is constructed. In a container that matters more than at a terminal. A typo in a deployment manifest is something you find out about from logs, and the difference between the container exiting immediately with that message and the program failing an hour later inside a request is most of the debugging. A non-zero exit at startup is also what a scheduler understands: the pod fails, it does not sit there serving errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the environment stops
&lt;/h2&gt;

&lt;p&gt;Three limits, all worth knowing before you build a deployment on this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A misspelled variable is silence.&lt;/strong&gt; &lt;code&gt;--min_mag=6&lt;/code&gt; on the command line is an error, because the parser knows the option does not exist. &lt;code&gt;QUAKES_MIN_MAG=6&lt;/code&gt; is not, because a process's environment is full of variables that are none of the program's business and it cannot tell yours from the rest:&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="nv"&gt;QUAKES_MIN_MAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6 quakes &lt;span class="nt"&gt;--print_config&lt;/span&gt; count
&lt;span class="go"&gt;distance_unit: mi
min_magnitude: 4.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The run proceeds with the file's value and no complaint. This is inherent to the mechanism rather than particular to jsonargparse, and &lt;code&gt;--print_config&lt;/code&gt; is the check: if the value you set is not in the output, the name is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An empty variable is a value, not an absence.&lt;/strong&gt; &lt;code&gt;QUAKES_MIN_MAGNITUDE=&lt;/code&gt; sets it to the empty string, which fails to parse as a float:&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="nv"&gt;QUAKES_MIN_MAGNITUDE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; quakes count
&lt;span class="go"&gt;error: Parser key "min_magnitude":
&lt;/span&gt;&lt;span class="gp"&gt;  Expected a &amp;lt;class 'float'&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; Got value:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shell scripts and CI templates produce empty variables easily, from an unset substitution or a job input nobody filled in. Unset the variable rather than blanking it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A structured value is one variable, not several.&lt;/strong&gt; &lt;code&gt;--area&lt;/code&gt; is the &lt;code&gt;Area&lt;/code&gt; dataclass from part 1, and on the command line it takes either a JSON document or its fields as separate options, &lt;code&gt;--area.latitude=35.7&lt;/code&gt;. Through the environment only the first of those exists: the help lists &lt;code&gt;QUAKES_SEARCH__AREA&lt;/code&gt; and nothing for &lt;code&gt;latitude&lt;/code&gt;, so the entire dataclass arrives in one variable.&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="nv"&gt;QUAKES_SEARCH__AREA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"latitude": 35.7, "longitude": 139.7}'&lt;/span&gt; quakes search &lt;span class="nt"&gt;--limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2
&lt;span class="go"&gt;time              magnitude  depth   latitude  longitude  id          place
2026-08-20 05:28  4.3        29.68   36.651    140.6399   us6000tm2a  5 km N of Hitachi, Japan
2026-08-17 07:36  4.6        230.19  34.8761   135.6311   usd0015ecj  3 km NNE of Takatsuki, Japan
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is parsed and checked like every other value — leave out &lt;code&gt;longitude&lt;/code&gt; and the run stops at startup saying so, rather than the dataclass being built without it. What does not improve is the writing: a JSON document squeezed into a shell variable is unpleasant to quote and hard to read in a manifest, and it gets worse the deeper the structure goes. Structure belongs in a config file, and &lt;code&gt;QUAKES_CONFIG&lt;/code&gt; exists so that a run can point at one. Environment variables are for the handful of scalars that differ between runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leaving it off
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;default_env=True&lt;/code&gt; is a decision, and the opposite one is defensible. It means the program's behaviour depends on variables that a user cannot see in the command they typed, which is a real cost when someone is debugging over a shoulder, and prefixed names collide with nothing but are still a namespace claimed in every shell the command runs in.&lt;/p&gt;

&lt;p&gt;If the tool is mostly used interactively, leave the argument out and let whoever needs it turn it on for a single run:&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_DEFAULT_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;quakes count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same mechanism as the &lt;code&gt;JSONARGPARSE_ADD_PRINT_COMPLETION_ARGUMENT&lt;/code&gt; in part 3, and the same reasoning: a behaviour the program's author might not want on by default is still available to the person running it, without a fork. Keeping &lt;code&gt;env_prefix&lt;/code&gt; set while leaving &lt;code&gt;default_env&lt;/code&gt; unset is a reasonable middle, since it fixes the names for whoever does enable it.&lt;/p&gt;

&lt;p&gt;One thing this post has not needed is a secret, because the earthquake catalog wants neither a key nor an account, which is why it makes such a convenient example. If it did want one, this is the mechanism it would arrive by, and the reason is the one part 2 gave for the config file: a token in a variable does not go into your shell history and does not get committed. What it does do is stay readable for as long as the container exists:&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;docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{json .Config.Env}}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$container&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="go"&gt;["QUAKES_MIN_MAGNITUDE=6","QUAKES_SUBCOMMAND=count","PATH=...","LANG=C.UTF-8",...]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anyone who can talk to the daemon can read that, and the same is true of &lt;code&gt;ps&lt;/code&gt; on some systems and of whatever your CI prints when it dumps the environment for a failed job. A file with restrictive permissions, or a secret mounted as one, is still the better place for a long lived credential — &lt;code&gt;QUAKES_CONFIG&lt;/code&gt; pointing at a mounted file is exactly that shape. The variable is the better place for a token that lives as long as the process does.&lt;/p&gt;

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

&lt;p&gt;The command now takes its settings from a file it finds on its own, a file named by a variable, individual variables, or the command line, and the same class decides what all four of them accept. Adding a parameter to &lt;code&gt;EarthquakeCatalog&lt;/code&gt; still adds an option, a config key, a completion and, now, an environment variable, and &lt;a href="https://github.com/mauvilsa/blog-earthquake-cli/blob/main/quakes_client.py" rel="noopener noreferrer"&gt;quakes_client.py&lt;/a&gt; still does not import jsonargparse.&lt;/p&gt;

&lt;p&gt;That is four posts of the same trade. Everything a command line tool conventionally maintains by hand — the parser, the help, the config schema, the completion script, the environment variable table — is a projection of something already written down in Python. Write it down accurately once, and the copies stop being copies.&lt;/p&gt;

</description>
      <category>python</category>
      <category>cli</category>
      <category>docker</category>
      <category>containers</category>
    </item>
    <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="https://github.com/mauvilsa/blog-earthquake-cli/blob/main/pyproject.toml" rel="noopener noreferrer"&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="https://github.com/mauvilsa/blog-earthquake-cli/blob/6f086e9094a38537c989c22769de824c95b56080/quakes_cli.py#L27-L28" rel="noopener noreferrer"&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="https://github.com/mauvilsa/blog-earthquake-cli/blob/main/quakes_client.py" rel="noopener noreferrer"&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>
