<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://lundybernard.github.io//feed.xml" rel="self" type="application/atom+xml" /><link href="https://lundybernard.github.io//" rel="alternate" type="text/html" /><updated>2026-02-06T20:57:05+00:00</updated><id>https://lundybernard.github.io//feed.xml</id><title type="html">{title of blog}</title><subtitle>My articles about software engineering,  programming, and open-source software.</subtitle><entry><title type="html">Configuration is a User Interface</title><link href="https://lundybernard.github.io//2026/01/23/config-is-ui.html" rel="alternate" type="text/html" title="Configuration is a User Interface" /><published>2026-01-23T19:00:00+00:00</published><updated>2026-01-23T19:00:00+00:00</updated><id>https://lundybernard.github.io//2026/01/23/config-is-ui</id><content type="html" xml:base="https://lundybernard.github.io//2026/01/23/config-is-ui.html"><![CDATA[<p>Configuration management is part of your User Interface.</p>

<p>Whether you are creating an application for users, a library for devs, 
or a microservice, how your software handles configuration 
is a critical concern and needs to cater to your users’ needs. 
Approaching configuration management <em>as</em> a part of the user interface 
will lead to better, user-friendly design decisions.</p>

<h2 id="what-is-configuration">What is Configuration?</h2>
<p>Configuration is part of a broader category of user-changeable inputs to software.</p>

<p>Configuration is user-controllable settings which change the 
behavior of the software at runtime. This includes things like logging levels, 
feature flags, and user preferences like dark/light mode. Configuration is provided 
either directly by the user (for example, CLI arguments) or from stored values, such as 
environment variables or config files.</p>

<p>Other kinds of user inputs are beyond the scope of configuration, such as plugins, 
data which the software will process, and complex user-defined logic like workflows, 
schemas, kubernetes manifests, or logstash pipelines.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">llm_model="gpt-oss"</code> is a config setting, the model file itself is not.</li>
  <li>Toggles for debug logging are a separate concern from data-processing rules which require their own DSL.</li>
</ul>

<h2 id="ui-characteristics-of-configuration">UI Characteristics of Configuration</h2>
<p>There is no hard and fast rule to distinguish configuration from other kinds of inputs.
What starts as a simple configuration can quickly grow into large complex
collections of settings.
It is helpful to keep GUI settings in mind when thinking about what belongs in the 
scope of configuration. If it is easy to include on a settings page 
(such as a dark mode toggle), it’s probably a good config option.
Often config options are short and simple enough for a user to type into a CLI, 
compared to more complex entries that you may want an IDE and syntax highlighting to manage.
Consider all of your’ users,</p>

<h2 id="how-will-your-users-set-configuration-options">How will your users set configuration options?</h2>
<p>That’s the critical question because the answer depends on who your users are
and their needs.
In a GUI or web-app it is common practice to include a ‘settings’ menu.
Config files are a common solution for many applications.
There are many other ways to configure software like:</p>
<ul>
  <li>command line arguments</li>
  <li>environment variables</li>
  <li>configuration store systems (Consul, etcd, Kubernetes ConfigMaps, etc.)</li>
</ul>

<p>Often, but not always, software libraries receive their config settings from
the code which utilizes them.</p>

<h2 id="practical-advice">Practical Advice:</h2>
<h3 id="think-of-config-as-part-of-your-ui">Think of config as part of your UI</h3>
<p>When someone edits your config file, they’re not “tinkering with internals”. 
They’re using an interface you designed — whether you meant to or not.
Since configuration deserves the same care as any other UI element,
it should:</p>
<ul>
  <li>be easy to understand</li>
  <li>be hard to misuse</li>
  <li>fail with helpful error messages</li>
  <li>be stable across versions
The best config UX assumes the user:</li>
  <li>is tired</li>
  <li>is in a hurry</li>
  <li>is operating the tool from a different context than the author</li>
  <li>will copy/paste examples</li>
  <li>will try to override settings in env vars</li>
</ul>

<h3 id="prefer-strings-at-the-edges-parse-inside-the-program">Prefer “strings at the edges” (parse inside the program)</h3>
<p>Configuration values enter the system as strings.
Your application turns them into the typed values that it needs.
Don’t make your users debug type mechanics. 
Handle parsing and validation in code.</p>

<p>This helps you craft helpful error messages like:
“TIMEOUT must look like 30s, 5m, or 250ms”
“THRESHOLD must be between 0 and 1”</p>

<p>This supports configuration using Environment variables, and is important for
users running software in containers, on cloud infrastructure, 
and other contexts ENV variables are preferred.</p>

<h3 id="be-wary-of-lists">Be wary of Lists</h3>
<p>Lists and key:value maps require special consideration.
It often makes sense to include lists of options in a configuration.
A short list in a CLI arg, or .yaml file can be simple and useful…
But lists are difficult to represent as strings, in Environment variables,
and require extra care when converting them to types.
Long lists (more than you want to type out by hand) probably represent Data
not configuration.</p>

<h3 id="interpolation-is-business-logic-dont-hide-logic-in-config">Interpolation is business logic (don’t hide logic in config)</h3>
<p>Interpolation and templating in config starts innocently:
“Let me reuse a base URL”
“Let me reference DATA_DIR”
“Let me compute a path”
Then it grows to include conditionals, defaults, string functions, 
environment lookups, precedence rules, edge cases around escaping…
It becomes its own language (DSL)</p>

<p>Configuration should declare inputs.
If values need to be derived, do the derivation in code where it can be tested.</p>

<h3 id="for-complex-user-logic-use-a-separate-system">For complex user logic, use a separate system</h3>
<p>If you require advanced user-defined logic, keep it separate from your runtime
config.  Build the appropriate testing and documentation around it, and avoid
conflating simple settings like dark mode and quiet output with programmatic logic.</p>

<p>keep the standard config small and boring (good!)
move complex, user-defined behavior into a separate lane:</p>
<ul>
  <li>plugins</li>
  <li>a dedicated rule file format with tooling</li>
  <li>a database-backed rules UI</li>
  <li>a sandboxed expression system with tests</li>
</ul>

<p>This separation protects your config UI 
from becoming a fragile all-purpose control panel.</p>

<h3 id="dont-confuse-configuration-with-data">Don’t confuse configuration with data</h3>
<p>This is one of the most expensive mistakes in “research tools that became real systems.”
Configuration answers: How should the tool run?
Data answers: What should the tool process?
If your config file starts containing lots of records, big lists, 
or evolving rule sets, it’s probably not config anymore—it’s data.</p>

<p>Config is not a substitute database.</p>

<h2 id="in-conclusion">In Conclusion</h2>
<p>Configuration is part of your user interface,
treat it like you would any other UI: 
keep it small, understandable, and hard to misuse.</p>

<p>In practice that means designing for the real world: 
multiple config sources (file, CLI, env vars),
users copying examples, and “future you” debugging a run at 2am. 
Favor simple, string-shaped inputs at the boundaries, 
validate and parse inside the program, and resist the temptation to smuggle
computation into configuration through interpolation.</p>

<p>And when your users truly need complex logic 
don’t force it into the same settings system. 
Give it a separate lane with the right tooling, validation, and tests.</p>

<p>Boring configuration is a feature. 
It makes your tool easier to run, easier to deploy, and easier to trust.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Configuration management is part of your User Interface.]]></summary></entry><entry><title type="html">Skunkworks: Isolating experimental code in production</title><link href="https://lundybernard.github.io//2025/10/12/skunky_code.html" rel="alternate" type="text/html" title="Skunkworks: Isolating experimental code in production" /><published>2025-10-12T19:00:00+00:00</published><updated>2025-10-12T19:00:00+00:00</updated><id>https://lundybernard.github.io//2025/10/12/skunky_code</id><content type="html" xml:base="https://lundybernard.github.io//2025/10/12/skunky_code.html"><![CDATA[<p>Sometimes we need to deal with substandard code in a release.
Using a <code class="language-plaintext highlighter-rouge">skunkworks</code> quarantine module to isolate it 
is a useful strategy when it is necessary.</p>

<p><em>“How will I know the good [code] from the bad?”
In the skunkworks module it will be</em></p>

<h2 id="why-would-you-allow-bad-code-to-be-merged">Why would you allow <em>bad</em> code to be merged?</h2>

<p>We Should™ only merge code which meets our quality and test coverage standards…</p>

<p>Sadly, this is not always practical or possible. 
There are times when a quick fix or a new feature needs to be merged ASAP.
Even after citing the <a href="http://www.laputan.org/mud/">Big Ball of Mud</a> pattern
as a dire warning, it may be necessary to merge a big chunk of messy untested
jank and hope for the best.</p>

<p>I don’t want to come across as too negative here,
because there are very good reasons to be flexible when it comes to code quality.
If there is an outage, getting back up and running is the top priority.
We do not want to block teammates or other teams by waiting on a perfect implementation.
There are many, many, real-world forces that legitimately drive us to cut corners.</p>

<h2 id="how-we-deal-with-bad-code">How we deal with <em>bad</em> code</h2>

<p>There are many strategies for dealing with this scenario, but in a crunch
where minutes matter, a few simple and ineffective strategies tend to dominate.</p>

<ul>
  <li>Mark it as TODO, or FIXME
    <ul>
      <li>It’s Fast!</li>
      <li>It’s part of the code, so we see it when working on that part of the codebase.</li>
      <li>But It’s invisible outside of the code, not part of our planning or issue tracking.</li>
      <li>And Easily ignored and put off till ‘later’</li>
    </ul>
  </li>
  <li>Create a cleanup ticket
    <ul>
      <li>It’s tracked and can be included in planning</li>
      <li>It can be included in reports on tech-debt</li>
      <li>But It’s invisible in the code.</li>
    </ul>
  </li>
</ul>

<p>These two quick and easy strategies work well together:
code comments can reference tickets, tickets support thorough explanations
and discussion about HOW TO “FIXME”.</p>

<h2 id="quarantine-that-code">Quarantine that code!</h2>
<p>My preferred solution is to create a new submodule named <code class="language-plaintext highlighter-rouge">skunkworks</code>
to contain any code which doesn’t meet quality and testing standards.</p>

<h3 id="how-to">How To:</h3>
<ol>
  <li>Create the submodule inside of your project source code.</li>
  <li>Exclude this module from code-quality requirements like
    <ul>
      <li>Test Coverage (instead of lowering your coverage requirements)</li>
      <li>Static type checking</li>
      <li>cyclic complexity checks</li>
    </ul>
    <ul>
      <li>Formatting requirements like Ruff and Black should probably still apply.</li>
    </ul>
  </li>
  <li>Add the quarantined code to skunkworks
    <ul>
      <li>Open a ticket to track the tech-debt</li>
      <li>Document the code to reference the ticket</li>
    </ul>
  </li>
  <li>Utilizing quarantined code now requires an import from skunkworks</li>
</ol>

<h3 id="advantages-of-this-approach">Advantages of this approach</h3>
<h4 id="use-of-quarantined-code-is-clearly-marked">Use of quarantined code is clearly marked</h4>
<p>Every time a developer sees <code class="language-plaintext highlighter-rouge">from sunkworks import ...</code> they know to be cautious.</p>

<p>When that code breaks, the traceback will clearly show that the exception bubbled up from skunkworks.</p>

<h4 id="avoiding-the-blame-game">Avoiding the blame game</h4>
<p>No one enjoys, or benefits from, the finger-pointing accusations and defenses 
that can arise when code fails catastrophically. 
Using an explicit quarantine module can help.</p>

<p>When your choose to create a <code class="language-plaintext highlighter-rouge">skunkworks</code> module,
you’re explicitly recognizing the need to take on tech-debt.
All of your stake-holders should be made aware, and know to expect problems.
It says “We need to use some substandard code. We acknowledge and accept the risks”</p>

<p>Everyone involved gets to share the responsibility, not just the author or approver of a specific pull request.
It is an architectural and managerial decision that the whole team makes together.</p>

<h4 id="cleanup">Cleanup!</h4>
<p>Putting all the untrusted code in one place makes finding what to clean up easy.
Moving code out of skunkworks can take precedence over all other cleanup and refactoring work.</p>

<p>It’s a great place for junior devs to start, tell them to “go clean out skunkworks”.
They can <code class="language-plaintext highlighter-rouge">git grep skunkworks</code> to see where its used.
You’re giving them working prototype-quality code, 
which they will need to write tests for,
refactor and clean up until it meets standards, and then merge into code base.</p>

<p>It’s easier to prioritize for cleanup.
Instead of arguing that “the {some subsystem} code is a mess, and we need to clean it up.”
Try “The code which {big important think people care about} relies on is still in skunkworks
and we need to get it out of there before something breaks”</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Sometimes we need to deal with substandard code in a release. Using a skunkworks quarantine module to isolate it is a useful strategy when it is necessary.]]></summary></entry><entry><title type="html">Supporting free-threading Python</title><link href="https://lundybernard.github.io//python/open-source/nogil/free-threading/2025/10/10/batconf-ft.html" rel="alternate" type="text/html" title="Supporting free-threading Python" /><published>2025-10-10T19:00:00+00:00</published><updated>2025-10-10T19:00:00+00:00</updated><id>https://lundybernard.github.io//python/open-source/nogil/free-threading/2025/10/10/batconf-ft</id><content type="html" xml:base="https://lundybernard.github.io//python/open-source/nogil/free-threading/2025/10/10/batconf-ft.html"><![CDATA[<p>How we made <a href="https://batconf.readthedocs.io/en/latest/#">BatConf</a> ready for free-threading/nogil</p>

<blockquote>
  <p><strong>TLDR</strong>:
BatConf is thread-safe, well tested, and ready for your free-threaded applications</p>
</blockquote>

<h2 id="threadsafe-by-design">Threadsafe by design</h2>
<p>Fortunately for us, configuration management is conceptually thread-safe.
We want to load configuration values from their source(s) once, and read them many times.
We do not expect configuration values to be written or updated at runtime.</p>

<p>BatConf’s architecture is essentially thread-safe:</p>
<ul>
  <li>Configuration Sources are treated as read-only</li>
  <li>Python itself protects us against read/write contention on variables</li>
</ul>

<h2 id="some-areas-for-concern">Some areas for concern</h2>
<p>“You’re far too trusting,” -Grand Moff Tarkin</p>

<p>While BatConf is conceptually and architecturally thread-safe,
it is not aggressively thread-safe.
Users are prohibited from creating attributes on a Configuration object,
updating Environment variables at runtime, 
or creating new config sources that are unsafe.</p>

<p>I don’t think we need to be overly concerned with users creating their own
concurrency problems. There is no hidden concurrency inside our package,
so any thread safety issues should be apparent in user code.</p>

<p>But what if we missed something in the design, and there’s some unexpected concurrency issue?</p>

<h2 id="reasonable-effort-to-ensure-thread-safety">Reasonable effort to ensure thread safety</h2>
<p>Simply saying “It Should™ be fine” is not very satisfying.
I would much rather put some tests in place.</p>

<h3 id="1-run-the-test-suite-with-free-threading-enabled">1. Run the test suite with free-threading enabled</h3>
<p>Since we already have an excellent set of test suites, 
running them with the GIL disabled 
gives us reasonable certainty that there are no major issues.</p>

<p>I was able to create a new Conda environment, install <code class="language-plaintext highlighter-rouge">python-freethreading</code>,
and run our test suite without any errors!</p>

<p>Then we added 3.14t to our CI testing matrix on github, everything passes.
Nice!</p>

<h3 id="2-real-threading-tests">2. Real threading tests</h3>
<p>Next we added a new suite of free-threading test cases.
These tests utilize BatConf Configuration objects in multiple threads.</p>

<p>They demonstrate the expected and officially supported use-cases,
provide examples, and guarantee that these uses-cases work both with and without the GIL.</p>

<h3 id="3-running-tests-in-parallel">3. Running tests in parallel</h3>
<p>Digging into the <a href="https://py-free-threading.github.io/">Python Free-Threading Guide</a>,
I was further assured that our pure-python package should be relatively safe, as
much more care needs to be taken with packages that include extensions in other languages.</p>

<p>There are several plugins for PyTest which allow us to run test cases in parallel,
including <a href="https://github.com/tonybaloney/pytest-freethreaded">pytest-freethreaded</a>
and <a href="https://github.com/Quansight-Labs/pytest-run-parallel">pytest-run-parallel</a>.</p>

<p>These failed spectacularly! Generating some surprising errors, and even locking up the pytest process!</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FAILED tests/integration/configuration_test.py::FreeFormConfigTreeTests::test_sub_configs_respect_environment_variables - KeyError: 'SHELL'
</code></pre></div></div>
<p>Wait… why is it looking for <code class="language-plaintext highlighter-rouge">SHELL</code>?  <code class="language-plaintext highlighter-rouge">git grep SHELL</code> confirms that doesn’t even appear in our code base…</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FAILED tests/example/example_test.py::CLITests::test_configuration_override_from_cli_args - AttributeError: '_patch' object has no attribute 'temp_original'
</code></pre></div></div>
<p>Errors bubbling up from the <code class="language-plaintext highlighter-rouge">_patch</code> object… uh oh.</p>

<p>While the package its self is thread-safe, the test suites are not.
We make extensive use of <code class="language-plaintext highlighter-rouge">unittest.mock.patch</code> to isolate objects under test from side effects,
and it is not thread-safe. 
Different threads compete to patch/unpatch the same objects.
Tests can run with patches from other tests in place.</p>

<p>Rewriting all our tests to be thread-safe is not on the roadmap.
Running all of our test suites together takes about 1.06s, so performance is not a concern.
Isolating test cases with mutex locks,
and refactoring the code to use more dependency injection for the sake of testing,
does not provide much value, and is unlikely to reveal thread-safety issues in the code itself.</p>

<h2 id="conclusion">Conclusion</h2>
<p>BatConf is ready for use in your free-threaded Python applications!</p>

<p>Free-threading in Python is the best thing since f-strings.
I can’t wait to build more truly parallel Python code, 
and develop new testing techniques to guarantee the safety and reliability of that code.</p>

<p>~ℒ</p>]]></content><author><name></name></author><category term="python" /><category term="open-source" /><category term="nogil" /><category term="free-threading" /><summary type="html"><![CDATA[How we made BatConf ready for free-threading/nogil]]></summary></entry><entry><title type="html">Adding Descriptors to Pandera’s Models</title><link href="https://lundybernard.github.io//python/open-source/2025/09/22/pandera-descriptors.html" rel="alternate" type="text/html" title="Adding Descriptors to Pandera’s Models" /><published>2025-09-22T19:00:00+00:00</published><updated>2025-09-22T19:00:00+00:00</updated><id>https://lundybernard.github.io//python/open-source/2025/09/22/pandera-descriptors</id><content type="html" xml:base="https://lundybernard.github.io//python/open-source/2025/09/22/pandera-descriptors.html"><![CDATA[<p>How and why I updated Pandera’s <a href="https://pandera.readthedocs.io/en/latest/dataframe_models.html">DataFrameModel</a>
to use Python’s descriptors for data attributes</p>

<blockquote>
  <p><strong>TLDR:</strong> Assigning <a href="https://docs.python.org/3/howto/descriptor.html">descriptors</a> to class attributes 
allows those attributes to behave like <a href="https://docs.python.org/3/library/functions.html#property">Properties</a>.
The values can be computed lazily, only when needed,
and referenced <code class="language-plaintext highlighter-rouge">.directly</code> instead of requiring a <code class="language-plaintext highlighter-rouge">.method_call()</code></p>
</blockquote>

<p><a href="https://github.com/unionai-oss/pandera/pull/2136/commits/a530024b0c2bf926333413f424f64cadbed3dada">skip to the recipe</a></p>

<h2 id="reusing-field-definitions">Reusing Field definitions</h2>
<p>While implementing type-checking for Pandas dataframes using Pandera,
I wanted to create re-usable Field definitions.
Repeating the definition of a <code class="language-plaintext highlighter-rouge">date</code> field in every <code class="language-plaintext highlighter-rouge">DataFrameModel</code> subclass
or using inheritance for individual fields felt clunky and was difficult to read.</p>

<p>It turns out each field in a DataFrameModel definition needs to be a unique instance.
You can read about the details <a href="https://github.com/unionai-oss/pandera/issues/1680">in this github issue</a>.</p>

<h3 id="the-solution-for-reusable-fields">The solution for reusable Fields</h3>
<p>With a little help from Niels Bantilan, the maintainer of Pandera, we came up with this solution
using partials to define reusable Field definitions</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">functools</span> <span class="kn">import</span> <span class="n">partial</span>
<span class="kn">from</span> <span class="nn">pandera</span> <span class="kn">import</span> <span class="n">DataFrameModel</span><span class="p">,</span> <span class="n">Field</span>

<span class="n">NormalizedField</span> <span class="o">=</span> <span class="n">partial</span><span class="p">(</span><span class="n">Field</span><span class="p">,</span> <span class="n">ge</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>

<span class="k">class</span> <span class="nc">GoodModelDF</span><span class="p">(</span><span class="n">DataFrameModel</span><span class="p">):</span>
    <span class="n">xnorm</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="n">NormalizedField</span><span class="p">()</span>
    <span class="n">ynorm</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="n">NormalizedField</span><span class="p">()</span>

<span class="k">class</span> <span class="nc">AlsoGoodModelDF</span><span class="p">(</span><span class="n">DataFrameModel</span><span class="p">):</span>
    <span class="n">xnorm</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="n">NormalizedField</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="debugging-and-surprising-behavior">Debugging, and surprising behavior</h2>
<p>While debugging the problems with reusable fields, I discovered the unexpected
behavior that motivated this change.</p>

<p>My first attempt to reuse a Field, resulted in a perplexing error:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">NormalizedField</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>

<span class="k">class</span> <span class="nc">BadModelDF</span><span class="p">(</span><span class="n">DataFrameModel</span><span class="p">):</span>
    <span class="n">field_0</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="n">GenericField</span>
    <span class="n">field_1</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="n">GenericField</span>  <span class="c1"># Bug: this breaks the model
</span>    
    <span class="k">class</span> <span class="nc">Config</span><span class="p">:</span>
        <span class="n">strict</span> <span class="o">=</span> <span class="bp">True</span>
</code></pre></div></div>
<p>Calling <code class="language-plaintext highlighter-rouge">BadModelDF.validate(some_dataframe)</code> raised the exception:
<code class="language-plaintext highlighter-rouge">SchemaError: column 'field_0' not in DataFrameSchema {'field_1': &lt;Schema Column(name=field_1, type=DataType(float64))&gt;</code>
<em>The root cause of that error is due to the behavior of <a href="https://github.com/unionai-oss/pandera/blob/ede8a4354cb41a5ef28218f5fbcf7bd64a761cf7/pandera/api/pandas/model.py#L69">DataFrameModel._build_columns_index</a>
, but is not relevant to this story.</em></p>

<p>Digging into the <a href="https://github.com/unionai-oss/pandera/blob/ede8a4354cb41a5ef28218f5fbcf7bd64a761cf7/pandera/api/dataframe/model.py#L116">DataFrameModel source code</a>
I saw that the <code class="language-plaintext highlighter-rouge">.__fields__</code> attribute should™ contain the data I’m looking for…
but when I viewed <code class="language-plaintext highlighter-rouge">BadModelDF.__fields__</code>, it was an empty dict <code class="language-plaintext highlighter-rouge">{}</code>!
And the <code class="language-plaintext highlighter-rouge">.__schema__</code> value was <code class="language-plaintext highlighter-rouge">None</code>.
This was frustrating… I defined a valid DataFrameModel, why would its attributes all be empty, uninitialized?</p>

<h3 id="the-deeply-unsatisfying-solution">The deeply unsatisfying solution</h3>
<p>The <code class="language-plaintext highlighter-rouge">DataFrameModel</code> class had a sort of secret <code class="language-plaintext highlighter-rouge">__init__</code> method.
You needed to call the <a href="https://github.com/unionai-oss/pandera/blob/ede8a4354cb41a5ef28218f5fbcf7bd64a761cf7/pandera/api/dataframe/model.py#L210">to_schema</a> 
method (or any other method which calls it) first,
because <code class="language-plaintext highlighter-rouge">.to_schema()</code> populates those data attributes.</p>

<h2 id="the-change">The change</h2>

<h3 id="motivation">Motivation</h3>

<p>Being deeply unsatisfied with this “spooky action at a distance”, I knew how it should™ work.
I expect the value of the attributes to always be correct.</p>

<p>We also don’t want to initialize these values when the class is interpreted,
because it is computationally expensive. 
There are cases where we will never use the computed values, 
for example when we set <code class="language-plaintext highlighter-rouge">PANDERA_VALIDATION_ENABLED=False</code>.
In fact, I don’t want to calculate the <code class="language-plaintext highlighter-rouge">__schema__</code> and all the other data attributes at all,
just to read the <code class="language-plaintext highlighter-rouge">__fields__</code> data.</p>

<h3 id="property-like-behavior-and-classes">Property-like behavior and classes</h3>
<p>IMHO: All™ data attributes on an object should behave like attributes, not methods.
I want to reference a value like <code class="language-plaintext highlighter-rouge">person.age &gt;= 21</code> not <code class="language-plaintext highlighter-rouge">person.get_current_age() &gt;= 21</code>.</p>

<p>Python <a href="https://docs.python.org/3/library/functions.html#property">Properties</a>
provide this behavior for computed values on class instances.</p>

<p>Sadly, properties do not work on classes, they work on instances of the class.
The interpreter reads a property definition like this:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">def</span> <span class="nf">MyClass</span><span class="p">:</span>
    <span class="o">@</span><span class="nb">property</span>
    <span class="k">def</span> <span class="nf">value</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="n">compute_the_value</span><span class="p">()</span>
</code></pre></div></div>
<p>something like “when I create an instance of this class, make its <code class="language-plaintext highlighter-rouge">value</code> attribute a property”.</p>

<p>Accessing a property of a class returns the <code class="language-plaintext highlighter-rouge">property</code> object <code class="language-plaintext highlighter-rouge">&lt;property at 0x###&gt;</code>,
not a computed value as we may have hoped.</p>

<p>Fortunately, <a href="https://docs.python.org/3/howto/descriptor.html">Descriptors</a>
allow us to write class attributes which behave just like properties!</p>

<h3 id="the-implementation">The Implementation</h3>
<p>A descriptor is a class, which has a <code class="language-plaintext highlighter-rouge">__get__</code> method with the appropriate signature.
We can assign an instance of the descriptor to a class attribute,
and it will behave just like a property, returning the computed value.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">In</span> <span class="p">[</span><span class="mi">1</span><span class="p">]:</span> <span class="k">class</span> <span class="nc">ValDesc</span><span class="p">:</span>
   <span class="p">...:</span>     <span class="k">def</span> <span class="nf">__get__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">obj</span><span class="p">,</span> <span class="n">objtype</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
   <span class="p">...:</span>         <span class="k">return</span> <span class="mi">42</span>
   <span class="p">...:</span> 

<span class="n">In</span> <span class="p">[</span><span class="mi">2</span><span class="p">]:</span> <span class="k">class</span> <span class="nc">SomeClass</span><span class="p">:</span>
   <span class="p">...:</span>     <span class="n">value</span> <span class="o">=</span> <span class="n">ValDesc</span><span class="p">()</span>
   <span class="p">...:</span> 

<span class="n">In</span> <span class="p">[</span><span class="mi">3</span><span class="p">]:</span> <span class="n">SomeClass</span><span class="p">.</span><span class="n">value</span>
<span class="n">Out</span><span class="p">[</span><span class="mi">3</span><span class="p">]:</span> <span class="mi">42</span>
</code></pre></div></div>

<h3 id="changes-to-the-pandera-dataclassmodel">Changes to the Pandera DataClassModel</h3>
<p>I had several goals for improving the Pandera Model interface:</p>
<ul>
  <li>Eliminate the need to call <code class="language-plaintext highlighter-rouge">.to_schema()</code> to populate data attribues.</li>
  <li>Data attributes like <code class="language-plaintext highlighter-rouge">__fields__</code> should™ be idempotent, and always provide valid values.</li>
  <li>Avoid unnecessary computation, by only computing the values we need, when we need them.</li>
</ul>

<p>To accomplish this, 
I added several Descriptor classes to the <code class="language-plaintext highlighter-rouge">DataClassModel</code>,
updated the old <code class="language-plaintext highlighter-rouge">get_</code> and <code class="language-plaintext highlighter-rouge">to_</code> class methods, and updated other class methods
to access the data attributes directly, while preserving the behavior of the API.</p>

<p>It worked! 
I improved the interface for Pandera’s Model class, eliminating a confusing behavior.
This change to a core component of the package was not disruptive, 
and the full test suite passed without modification.
We also achieved some marginal performance improvements.</p>

<p>You can read the <a href="https://github.com/unionai-oss/pandera/pull/2136/commits/a530024b0c2bf926333413f424f64cadbed3dada">full commit in the PR</a>.
This is a brief illustration of some of the changes:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">_ClassDescriptor</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">cache</span> <span class="o">=</span> <span class="p">{}</span>


<span class="k">class</span> <span class="nc">_FieldsDescriptor</span><span class="p">(</span><span class="n">_ClassDescriptor</span><span class="p">):</span>
    <span class="s">"""Descriptor which allows __fields__ to act as a class property."""</span>

    <span class="k">def</span> <span class="nf">__get__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">obj</span><span class="p">,</span> <span class="n">cls</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">TFields</span><span class="p">:</span>
        <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">cache</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">cls</span><span class="p">)</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">cache</span><span class="p">[</span><span class="n">cls</span><span class="p">]</span> <span class="o">=</span> <span class="n">cls</span><span class="p">.</span><span class="n">_collect_fields</span><span class="p">()</span>

            <span class="k">for</span> <span class="n">field</span><span class="p">,</span> <span class="p">(</span><span class="n">annot_info</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">cache</span><span class="p">[</span><span class="n">cls</span><span class="p">].</span><span class="n">items</span><span class="p">():</span>
                <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">annot_info</span><span class="p">.</span><span class="n">arg</span><span class="p">,</span> <span class="n">TypeVar</span><span class="p">):</span>
                    <span class="k">raise</span> <span class="n">SchemaInitError</span><span class="p">(</span>
                        <span class="sa">f</span><span class="s">"Field </span><span class="si">{</span><span class="n">field</span><span class="si">}</span><span class="s"> has a generic data type"</span>
                    <span class="p">)</span>

        <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">cache</span><span class="p">[</span><span class="n">cls</span><span class="p">]</span>


<span class="k">class</span> <span class="nc">_SchemaDescriptor</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__get__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">obj</span><span class="p">,</span> <span class="n">cls</span><span class="p">):</span>
        <span class="p">...</span>


<span class="k">class</span> <span class="nc">DataFrameModel</span><span class="p">(</span><span class="n">Generic</span><span class="p">[</span><span class="n">TDataFrame</span><span class="p">,</span> <span class="n">TSchema</span><span class="p">],</span> <span class="n">BaseModel</span><span class="p">):</span>
    <span class="p">...</span>
    <span class="n">__fields__</span><span class="p">:</span> <span class="n">ClassVar</span><span class="p">[</span><span class="n">TFields</span><span class="p">]</span> <span class="o">=</span> <span class="n">cast</span><span class="p">(</span><span class="n">TFields</span><span class="p">,</span> <span class="n">_FieldsDescriptor</span><span class="p">())</span>
    <span class="n">__schema__</span> <span class="o">=</span> <span class="n">_SchemaDescriptor</span><span class="p">()</span>
    <span class="p">...</span>

    <span class="c1"># Update class methods to use the new descriptors, instead of calling class methods
</span>    <span class="o">@</span><span class="nb">classmethod</span>
    <span class="k">def</span> <span class="nf">to_schema</span><span class="p">(</span><span class="n">cls</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">TSchema</span><span class="p">:</span>
        <span class="s">"""Create :class:`~pandera.DataFrameSchema` from the :class:`.DataFrameModel`."""</span>
        <span class="k">return</span> <span class="n">cls</span><span class="p">.</span><span class="n">__schema__</span>

    <span class="o">@</span><span class="nb">classmethod</span>
    <span class="k">def</span> <span class="nf">to_yaml</span><span class="p">(</span><span class="n">cls</span><span class="p">,</span> <span class="n">stream</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">os</span><span class="p">.</span><span class="n">PathLike</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">):</span>
        <span class="s">"""
        Convert `Schema` to yaml using `io.to_yaml`.
        """</span>
        <span class="k">return</span> <span class="n">cls</span><span class="p">.</span><span class="n">__schema__</span><span class="p">.</span><span class="n">to_yaml</span><span class="p">(</span><span class="n">stream</span><span class="p">)</span>

    <span class="p">...</span>
</code></pre></div></div>

<p>Thanks for reading,</p>

<p>Lundy</p>]]></content><author><name></name></author><category term="python" /><category term="open-source" /><summary type="html"><![CDATA[How and why I updated Pandera’s DataFrameModel to use Python’s descriptors for data attributes]]></summary></entry></feed>