<?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: Kabir Hossain</title>
    <description>The latest articles on DEV Community by Kabir Hossain (@praman).</description>
    <link>https://dev.to/praman</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%2F1391841%2F20a0eebf-df9a-4369-ba79-a21f87d70b31.png</url>
      <title>DEV Community: Kabir Hossain</title>
      <link>https://dev.to/praman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/praman"/>
    <language>en</language>
    <item>
      <title>Config precedence in Python: why your resolver shouldn't know what "CLI" means</title>
      <dc:creator>Kabir Hossain</dc:creator>
      <pubDate>Wed, 14 Oct 2026 03:43:48 +0000</pubDate>
      <link>https://dev.to/praman/config-precedence-in-python-why-your-resolver-shouldnt-know-what-cli-means-1aci</link>
      <guid>https://dev.to/praman/config-precedence-in-python-why-your-resolver-shouldnt-know-what-cli-means-1aci</guid>
      <description>&lt;p&gt;Most config systems I've written have the same shape. A function that looks at the environment, looks at the command line, looks at a YAML file, and decides which one wins. Something like 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;load_config&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;cli_args&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;return&lt;/span&gt; &lt;span class="n"&gt;cli_args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TIMEOUT&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="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TIMEOUT&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="n"&gt;yaml_config&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;timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multiply that by every setting and you get a function that's 200 lines long, impossible to test in isolation, and impossible to change precedence on without editing every branch.&lt;/p&gt;

&lt;p&gt;The version I've been working on separates two things the if/else version fuses together. What order sources apply in. And how they merge. Once those are separate, precedence stops being code and starts being structure.&lt;/p&gt;

&lt;p&gt;The resolver doesn't know what "CLI" means. Here's the merge function in full:&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;resolve_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SourceSpec&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;merged_config&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;spec&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sources&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="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_config&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&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;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failure_policy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Optional configuration source %s failed: %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__class__&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="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;merged_config&lt;/span&gt; &lt;span class="o"&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;merged_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;config&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;merged_config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that and notice what it doesn't do. It doesn't check whether a source is a CLI source, an env source, or a YAML source. It doesn't sort anything. It doesn't decide that one kind of source should win over another kind. It takes a list of sources, merges them left to right, and returns the result.&lt;/p&gt;

&lt;p&gt;The last source in the list wins. That's the entire precedence rule.&lt;/p&gt;

&lt;p&gt;That's not a limitation. It's the design. The moment the resolver knows what "CLI" means, it can start making decisions it shouldn't. It can start reordering. It can start special-casing. And once it does that, you can't test precedence without also testing the resolver's opinions, and you can't change precedence without reading the resolver's code.&lt;/p&gt;

&lt;p&gt;Precedence is a list. That list comes from a builder:&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;build_standard_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_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="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cli_args&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;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;yaml_source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;YAMLSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;env_source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EnvSource&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cli_source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CLISource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cli_args&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="nc"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;yaml_source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;env_source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OPTIONAL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cli_source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OPTIONAL&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;YAML first. Env second. CLI last. Since the resolver merges left-to-right and later sources override earlier ones, CLI wins over env wins over YAML. That's the whole precedence system, and it's expressed as three lines of a list.&lt;/p&gt;

&lt;p&gt;If you want to change precedence, say you decide env should beat CLI, you change the order of that list. Nothing else changes. No if branches to hunt down. No settings that read precedence from a config file. Just the order of the list.&lt;/p&gt;

&lt;p&gt;The tests are where the pattern earns its keep, because they lock in behaviors, not implementation details.&lt;/p&gt;

&lt;p&gt;First test. Later source wins.&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;test_1_later_source_wins&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;source_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeConfigSource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&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;chromium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;source_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeConfigSource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&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;firefox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;source_c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeConfigSource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&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;webkit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;specs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;source_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;source_b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;source_c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;specs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&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;webkit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the base contract. Merge is left-to-right, last write wins. It's obvious in isolation and it's the thing every other behavior depends on.&lt;/p&gt;

&lt;p&gt;Second test. Reordering changes result.&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;test_2_reordering_changes_result&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;source_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeConfigSource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&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;chromium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;source_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeConfigSource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&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;firefox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;source_c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeConfigSource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&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;webkit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;specs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;source_c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;source_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;source_b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;specs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&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;firefox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one looks trivial and isn't. It proves the resolver has no hidden opinion about which source should win. If someone ever helpfully adds logic to the resolver that reorders sources, say putting CLI sources last regardless of where they appear, this test fails. That's the category of mistake it prevents. Someone trying to make precedence smarter inside the merge.&lt;/p&gt;

&lt;p&gt;Third test. The resolver doesn't sort.&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;test_3_resolver_does_not_sort&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Simulate a logically reversed order: CLI-like -&amp;gt; YAML-like -&amp;gt; ENV-like
&lt;/span&gt;    &lt;span class="c1"&gt;# The resolver should NOT know these names mean anything.
&lt;/span&gt;    &lt;span class="n"&gt;cli_like&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeConfigSource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&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;webkit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;yaml_like&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeConfigSource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&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;chromium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;env_like&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeConfigSource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&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;firefox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;specs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cli_like&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;yaml_like&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;env_like&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;specs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&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;firefox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the one I'd defend hardest. It uses three sources named after the standard precedence chain, CLI, YAML, ENV, but passes them in a deliberately wrong order. The resolver's job is to merge them in the order given, not to fix it.&lt;/p&gt;

&lt;p&gt;If you ever find yourself wanting to add precedence knowledge to the resolver, this test is the one that will stop you. That's the point. Tests aren't just assertions about current behavior. They're arguments against future versions of yourself who will have worse ideas.&lt;/p&gt;

&lt;p&gt;There's a subtle thing in the resolver that's easy to miss.&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;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_config&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&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;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failure_policy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each source carries its own failure policy. YAML is required. If the file is missing or malformed, the pipeline fails. Env and CLI are optional. If they fail to load, the pipeline continues with what it has.&lt;/p&gt;

&lt;p&gt;This is deliberately a separate concern from precedence. A source can be last in precedence, highest priority, and optional, failing silently. A source can be first in precedence, lowest priority, and required. The two axes are independent.&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;test_5_failure_policy_remains_independent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caplog&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;valid_yaml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeConfigSource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&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;chromium&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;timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;failing_env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FailingSource&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;valid_cli&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeConfigSource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&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;webkit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;specs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;valid_yaml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;failing_env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OPTIONAL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;SourceSpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;valid_cli&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUIRED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;caplog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at_level&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WARNING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;specs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&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;webkit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeout&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;30000&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Optional configuration source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;caplog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Env fails. The pipeline continues. CLI still overrides YAML. Timeout from YAML still applies because nothing else set it. Precedence and failure handling do their jobs without interacting.&lt;/p&gt;

&lt;p&gt;Fusing these two concerns is the most common mistake I see in config systems. It looks natural. If the CLI arg is missing, fall back to env, which falls back to YAML. But it collapses two orthogonal decisions into one tangled function. Keeping them separate makes each one testable in isolation and changeable without touching the other.&lt;/p&gt;

&lt;p&gt;One more test, because it's the one that guards the whole design:&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;test_7_resolver_remains_unchanged&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&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;signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolve_config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;assert&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;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;params&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;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;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This asserts that resolve_config takes exactly one parameter. Nothing else. No pipeline name, no environment, no flags. Just the list of sources.&lt;/p&gt;

&lt;p&gt;It sounds pedantic, and it is, deliberately. Six months from now, someone (probably me) will want to pass an Environment into the resolver so it can adjust precedence based on the environment. This test makes that change fail loudly, which forces a conversation about whether the resolver should know anything about environments. The answer is almost always no. But without the test, the answer quietly becomes yes, one small change at a time.&lt;/p&gt;

&lt;p&gt;What this pattern buys you.&lt;/p&gt;

&lt;p&gt;Precedence is a list. Changing it means reordering a list, not editing logic.&lt;/p&gt;

&lt;p&gt;The resolver does one thing. It merges dictionaries left to right. That's it. It's 20 lines and it doesn't know what a CLI is.&lt;/p&gt;

&lt;p&gt;Failure handling is orthogonal. Each source declares whether it's required or optional. The resolver respects that declaration without conflating it with priority.&lt;/p&gt;

&lt;p&gt;Tests lock behaviors, not implementations. Each test asserts something category-level about what the system should never do, not just what it currently does.&lt;/p&gt;

&lt;p&gt;None of this is novel. It's the same pattern behind a dozen config libraries. But the version where precedence is structure, rather than a decision buried in a conditional, is the version I've found easiest to live with. Because the day you need to change precedence is the day you least want to be reading the resolver's code.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>programming</category>
      <category>python</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How I scrape any website in under 5 minutes (Python + Playwright)</title>
      <dc:creator>Kabir Hossain</dc:creator>
      <pubDate>Wed, 07 Oct 2026 04:48:09 +0000</pubDate>
      <link>https://dev.to/praman/how-i-scrape-any-website-in-under-5-minutes-python-playwright-25d3</link>
      <guid>https://dev.to/praman/how-i-scrape-any-website-in-under-5-minutes-python-playwright-25d3</guid>
      <description>&lt;p&gt;python, webdev, tutorial, automation&lt;br&gt;
Every scraping tutorial I've read online ends the same way.&lt;/p&gt;

&lt;p&gt;"Here's how to grab a title with BeautifulSoup." Twenty lines of Python. Works on the demo page. Fails on a real website.&lt;/p&gt;

&lt;p&gt;That bothered me for a long time. Because real websites are messy. They use JavaScript. They block bots. They hide data behind logins. They paginate. They rename their HTML classes every few months and break every script you've written.&lt;/p&gt;

&lt;p&gt;So instead of writing another "20-line scraper" tutorial, I want to show you what actual production scraping looks like. Not the theory — the workflow I use for real client jobs.&lt;/p&gt;

&lt;p&gt;What most tutorials skip&lt;br&gt;
When someone asks me to scrape a site, I don't open a fresh Python file and start writing selectors. I follow a process.&lt;/p&gt;

&lt;p&gt;Step one is an audit. I check the site first. Is it static HTML or JavaScript-rendered? Does it have clear repeating elements like product cards? Does it paginate cleanly? Does robots.txt allow scraping?&lt;/p&gt;

&lt;p&gt;If I skip this step, I waste hours trying to scrape a site that was never going to work. Five minutes of auditing saves five hours of frustration.&lt;/p&gt;

&lt;p&gt;Building the audit as a tool&lt;br&gt;
Since I audit every site the same way, I automated the audit itself.&lt;/p&gt;

&lt;p&gt;Here's what my tool does when I run it:&lt;/p&gt;

&lt;p&gt;bash&lt;br&gt;
tendem-audit&lt;br&gt;
It asks for a URL. Then it:&lt;/p&gt;

&lt;p&gt;Fetches the page using the same fetch strategy I use for scraping (static HTTP first, Playwright second)&lt;/p&gt;

&lt;p&gt;Checks robots.txt&lt;/p&gt;

&lt;p&gt;Auto-detects the repeating card block using heuristics&lt;/p&gt;

&lt;p&gt;Fingerprints the CMS (Shopify, WooCommerce, WordPress, Magento)&lt;/p&gt;

&lt;p&gt;Prints a verdict: green, yellow, orange, or red&lt;/p&gt;

&lt;p&gt;Green means I can quote the job today. Red means I politely decline.&lt;/p&gt;

&lt;p&gt;That one step — automating the audit — saved me more time than any scraper I've written.&lt;/p&gt;

&lt;p&gt;The actual scraping&lt;br&gt;
Once I know the site is scrapeable, the scraping itself is boring. That's how it should be.&lt;/p&gt;

&lt;p&gt;bash&lt;br&gt;
tendem-scrape &lt;a href="https://shop.example.com/products" rel="noopener noreferrer"&gt;https://shop.example.com/products&lt;/a&gt; --preset shopify --max-pages 50 --format csv,json,sqlite&lt;br&gt;
That one command:&lt;/p&gt;

&lt;p&gt;Fetches all 50 pages automatically by following the "Next" link&lt;/p&gt;

&lt;p&gt;Uses the WooCommerce preset to find the right fields (title, price, image, link)&lt;/p&gt;

&lt;p&gt;Handles the pagination URL changes&lt;/p&gt;

&lt;p&gt;Merges items into one dataset&lt;/p&gt;

&lt;p&gt;Writes CSV, JSON, and SQLite files&lt;/p&gt;

&lt;p&gt;Total time: two minutes. For a 1,000-product catalog.&lt;/p&gt;

&lt;p&gt;What production scraping actually means&lt;br&gt;
The scraping part is 20% of the job. The other 80% is what makes a client pay.&lt;/p&gt;

&lt;p&gt;Here's what I deliver for every scraping job — no exceptions:&lt;/p&gt;

&lt;p&gt;Clean CSV, ready to open in Excel&lt;/p&gt;

&lt;p&gt;Not a raw dump. Columns with the right names. No hidden characters. UTF-8 BOM so Excel doesn't mangle it.&lt;/p&gt;

&lt;p&gt;Interactive HTML dashboard&lt;/p&gt;

&lt;p&gt;A single file the client opens in their browser. Sortable columns. Filterable search box. Works offline. No dependency on my server.&lt;/p&gt;

&lt;p&gt;Data quality report&lt;/p&gt;

&lt;p&gt;A file that lists exactly what problems I found: missing titles, empty prices, duplicate rows, broken image links. Clients trust the data because they can see what was checked.&lt;/p&gt;

&lt;p&gt;Audit trail&lt;/p&gt;

&lt;p&gt;What was scraped, when, from what URL, by what method. If the client asks "how did you get this?", the answer is one JSON file away.&lt;/p&gt;

&lt;p&gt;The technical stack&lt;/p&gt;

&lt;p&gt;For those who want to know how it works under the hood:&lt;/p&gt;

&lt;p&gt;Python 3.10 or newer&lt;/p&gt;

&lt;p&gt;Playwright for anything JavaScript-rendered&lt;/p&gt;

&lt;p&gt;BeautifulSoup with lxml for parsing&lt;/p&gt;

&lt;p&gt;Pydantic v2 for validated data models&lt;/p&gt;

&lt;p&gt;SQLite or PostgreSQL for storage&lt;/p&gt;

&lt;p&gt;GitHub Actions for the CI pipeline&lt;/p&gt;

&lt;p&gt;Pytest and Allure for tests and reports&lt;/p&gt;

&lt;p&gt;The framework has over 70 automated tests. There's a public test dashboard on GitHub Pages that shows them passing.&lt;/p&gt;

&lt;p&gt;What I learned doing this for real clients&lt;br&gt;
Three things stand out.&lt;/p&gt;

&lt;p&gt;First, sessions and logins matter more than anything. Half the sites I scrape require a login. If you don't learn how to save cookies and reuse them, half the job disappears.&lt;/p&gt;

&lt;p&gt;Second, quality control is what clients actually pay for. A raw CSV is worth ten dollars. A CSV plus a quality report plus a dashboard is worth a hundred. Same data, different packaging.&lt;/p&gt;

&lt;p&gt;Third, when a site changes, your tests catch it before the client does. That's not a nice-to-have — it's the difference between a one-off job and a monthly subscription.&lt;/p&gt;

&lt;p&gt;Try it yourself&lt;br&gt;
bash&lt;br&gt;
git clone &lt;a href="https://github.com/pranromumu/tendem-scraper" rel="noopener noreferrer"&gt;https://github.com/pranromumu/tendem-scraper&lt;/a&gt;&lt;br&gt;
cd tendem-scraper&lt;br&gt;
python -m venv .venv&lt;br&gt;
source .venv/bin/activate&lt;br&gt;
pip install -e ".[dev]"&lt;br&gt;
playwright install chromium&lt;/p&gt;

&lt;p&gt;tendem-scrape &lt;a href="https://books.toscrape.com/" rel="noopener noreferrer"&gt;https://books.toscrape.com/&lt;/a&gt; --max-pages 3 --open&lt;br&gt;
The last command scrapes three pages and opens the dashboard in your browser.&lt;/p&gt;

&lt;p&gt;What's next&lt;br&gt;
I'm building this out as a service for e-commerce sellers and marketing agencies who need competitor pricing, product catalogs, and lead lists on a schedule. If you have a scraping project in mind, or you just want to talk about Python automation, feel free to reach out.&lt;/p&gt;

&lt;p&gt;The GitHub repo is open source. Fork it, break it, submit a pull request.&lt;/p&gt;

&lt;p&gt;About me: I'm a web scraping and automation engineer based in Malaysia. I build Python tools that extract data from websites and deliver clean, structured files with quality guarantees.&lt;/p&gt;

&lt;p&gt;Live demo: &lt;a href="https://pranromumu.github.io/tendem-scraper/" rel="noopener noreferrer"&gt;https://pranromumu.github.io/tendem-scraper/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub repo: &lt;a href="https://github.com/pranromumu/tendem-scraper" rel="noopener noreferrer"&gt;https://github.com/pranromumu/tendem-scraper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fiverr gig: &lt;a href="https://www.fiverr.com/s/3AAA3yL" rel="noopener noreferrer"&gt;https://www.fiverr.com/s/3AAA3yL&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you found this post useful, connect with me. I write about Python, scraping, and automation for real client work.&lt;/p&gt;

</description>
      <category>python</category>
      <category>tutorial</category>
      <category>webdev</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>From Script to Secure Pipeline: How I Created a "Trust Boundary" in CI/CD</title>
      <dc:creator>Kabir Hossain</dc:creator>
      <pubDate>Fri, 25 Sep 2026 06:33:41 +0000</pubDate>
      <link>https://dev.to/praman/from-script-to-secure-pipeline-how-i-created-a-trust-boundary-in-cicd-11em</link>
      <guid>https://dev.to/praman/from-script-to-secure-pipeline-how-i-created-a-trust-boundary-in-cicd-11em</guid>
      <description>&lt;p&gt;A few months ago, I thought CI/CD simply meant writing a GitHub Actions YAML file that runs &lt;code&gt;pytest&lt;/code&gt;.&lt;br&gt;
If tests pass, the result is 'green'; if they fail, it's 'red'pretty straightforward, right?&lt;br&gt;
However, while progressing through my QA automation roadmap (projects P141–P170), my mentor introduced me to a concept that completely shifted my perspective on testing: the 'Trust Boundary'.&lt;br&gt;
When a test fails in CI, developers need evidence (such as screenshots, traces, or logs) to debug the issue. But how can they be certain that the evidence stored hasn't been corrupted? How do they know the pipeline is making the correct decision?&lt;br&gt;
Here is how I built a secure CI pipeline to address this challenge.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Preserving Evidence of Failure (The &lt;code&gt;if: always()&lt;/code&gt;Strategy)
Typically, if a GitHub Actions job fails, subsequent steps do not execute. This means that if your Playwright test crashes, the step to upload artifacts won't run-causing the failure screenshot to be lost forever as soon as the virtual machine shuts down.
To solve this, I used the &lt;code&gt;if: always()&lt;/code&gt; condition for the upload step:
-name: Upload Failure Evidence
uses: actions/upload-artifact@v4
if: always()
with:
name: test-evidence
path: artifacts/
This ensures that even if the test crashes, the evidence is safely uploaded to GitHub's cloud storage.&lt;/li&gt;
&lt;li&gt;Independent Auditor/Verifier (Decoupled from the Test Job)
If the test job itself generates the evidence and also verifies it, the situation becomes akin to grading one's own homework. Even if a bug corrupts the file, the test job might still report a successful verification (&lt;code&gt;verification = true&lt;/code&gt;) based on incorrect data.
To resolve this, I split my pipeline into separate jobs:
Test Job: Runs Pytest, generates evidence, calculates the SHA-256 hash, and uploads it to the cloud.
Verify Job: Downloads the artifact, recalculates the hash from the raw bytes, and compares the two.
This establishes a 'trust boundary.' Here, the Verify job acts as an independent auditor. If anyone (or a system bug) alters or tampers with the evidence while it is stored in the cloud, the hashes will no longer match, and the auditor will mark the pipeline as 'failed.'&lt;/li&gt;
&lt;li&gt;The Final Quality Gate
Finally, I added a third job: 'The Gate.'
This job depends on the 'Verify' job (though the &lt;code&gt;if: always()&lt;/code&gt; condition was not used). Its sole purpose is to display the final decision:
=================================
FINAL EVIDENCE GATE
=================================
Test execution:      PASS
Artifact upload:     PASS
SHA-256 verification: PASS
FINAL GATE: PASS 
If verification fails, the 'Gate' step is skipped, and the overall workflow is marked as 'Red' (failed).
What's next?"Praman"(Proof) 
By completing this stage of CI/CD, I realized that QA isn't just about finding bugs; it is about building an engineering framework based on trust and evidence.
Stemming from this insight, I’ve named my future architecture'Praman'-a word that translates to 'Proof' or 'Evidence'.
The next steps in my roadmap involve transforming this pipeline into a modular, "drop-in" Python QA engine that freelance clients can easily integrate into their own repositories. It will be a tool designed to execute tests, gather irrefutable evidence, and (eventually) leverage AI to analyze the root causes of failures.
If you are building CI/CD pipelines, I highly recommend exploring independent artifact verification. It elevates your pipeline from a mere "script runner" into a professional-grade engineering system.
What aspects of CI/CD are you currently learning? Let’s connect! &lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>security</category>
      <category>testing</category>
    </item>
    <item>
      <title>My QA Automation Journey: From Zero to CI/CD 🚀</title>
      <dc:creator>Kabir Hossain</dc:creator>
      <pubDate>Mon, 21 Sep 2026 22:43:34 +0000</pubDate>
      <link>https://dev.to/praman/my-qa-automation-journey-from-zero-to-cicd-3fdl</link>
      <guid>https://dev.to/praman/my-qa-automation-journey-from-zero-to-cicd-3fdl</guid>
      <description>&lt;p&gt;Hello, &lt;br&gt;
I am embarking on a long journey into QA automation—and documenting my experiences along the way.&lt;br&gt;
Just a few months ago, while I could run &lt;code&gt;pytest&lt;/code&gt;, I lacked a clear understanding of what a professional automation framework actually looks like.&lt;br&gt;
I was familiar with some basic concepts of Python and automation. However, there was a significant gap between knowing isolated tools and understanding how everything integrates within a real-world QA engineering workflow.&lt;br&gt;
So, I decided to bridge that gap through hands-on practice.&lt;br&gt;
I am following a roadmap comprising over 200 practical, hands-on projects. It begins with Python and Playwright, progressing through CI/CD, observability, and AI-assisted testing, ultimately leading to the creation of an AI-driven QA platform.&lt;br&gt;
Here is what I have worked on so far:&lt;br&gt;
✅ Python Fundamentals&lt;br&gt;
✅ Playwright&lt;br&gt;
✅ Pytest&lt;br&gt;
✅ Page Object Model&lt;br&gt;
✅ API Testing&lt;br&gt;
✅ Authentication and Session Handling&lt;br&gt;
✅ Network Interception&lt;br&gt;
✅ Screenshots, Traces, Videos, and HTML Reports&lt;br&gt;
✅ GitHub Actions and CI/CD&lt;/p&gt;

&lt;p&gt;I am currently approaching Project 152 (P152).&lt;br&gt;
Through this process, I have realized something important: learning QA automation isn't just about learning how to write a test.&lt;br&gt;
It is equally crucial to understand what happens when a test fails. For instance, I recently set up a CI 'quality gate' for a project where the process went like this:&lt;/p&gt;

&lt;p&gt;Tests fail&lt;br&gt;
↓&lt;br&gt;
CI pipeline fails&lt;br&gt;
↓&lt;br&gt;
Evidence or data regarding the failure is collected&lt;br&gt;
↓&lt;br&gt;
Artifacts are uploaded&lt;/p&gt;

&lt;p&gt;If a test fails, the pipeline should fail too—but I didn't want the evidence or data concerning that failure to be lost along with the pipeline itself.&lt;br&gt;
It was a small project, certainly, but it taught me a valuable lesson about how a real-world CI pipeline should function.&lt;br&gt;
Throughout this journey, I will continue to document my project, mistakes, problem-solving approaches, and everything new I learn.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>cicd</category>
      <category>python</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
