Same Words, Different Dictionary

▶ Listen to this article

I was generating KiCad schematics programmatically — writing a Python script that emits .kicad_sch files directly, s-expression by s-expression. The format is well-documented, the structure is regular, and it was going fine until this:

Expected one of: input, output, bidirectional, tri_state, passive
Got 'power_in'

Here’s the thing: power_in is a perfectly valid KiCad value. I didn’t make it up. I didn’t misspell anything. The word exists in the format spec, it’s used throughout the file, and KiCad itself generates it. So why is the parser choking on it?

Because power_in is valid in one context and illegal in another, and the format gives you no signal that you’ve crossed the border.

Two Vocabularies, One Format

KiCad’s schematic format uses s-expressions — nested parenthesized structures, like a stripped-down Lisp. Inside a symbol library definition, you declare pin electrical types:

(pin power_in line
  (at 0 0) (length 2.54)
  (name "VCC" (effects (font (size 1.27 1.27)))))

That power_in tells KiCad this pin is a power input — it draws current, it expects a voltage source, the ERC checker uses it to validate net connections. The full set of valid electrical types is generous: input, output, passive, power_in, power_out, bidirectional, tri_state, unspecified, open_collector, open_emitter, no_connect. Eleven options.

Now step outside the symbol definition into the schematic itself. When you place a hierarchical label — a named connection that links child sheets to parent sheets — you specify a shape:

(hierarchical_label "VCC"
  (shape input)
  (at 25.4 50.8)
  ...)

That shape controls the visual indicator on the label: an arrow for input, a reversed arrow for output, a diamond for bidirectional. The valid set here is: input, output, bidirectional, tri_state, passive. Five options. No power_in. No power_out.

Same format. Same file. Some of the same words. Different dictionaries.

Why This Is Hard to Catch

My generator script was doing the obvious thing: when creating a hierarchical label for a power pin, it passed the pin’s electrical type straight through as the label shape. power_in pin becomes power_in label. The code reads clean. The logic feels correct. A power input pin should get marked as an input. And it does — just not with the word power_in.

The trap is that the valid value sets overlap. Five of the eleven pin types (input, output, bidirectional, tri_state, passive) are also valid label shapes. They happen to mean analogous things in both contexts. So if your power net were instead a passive connection, the same straight-through code would work perfectly. You’d never know there was a boundary you were crossing.

This is a specific flavor of bug that’s easy to introduce and hard to spot: vocabulary collision across semantic contexts. The words look right. The structure is syntactically valid. The parser only complains when the specific value you’re using happens to fall outside the intersection of both sets. Test with input and everything passes. Ship with power_in and it detonates in production.

The Fix

Once you see it, the fix is almost insultingly simple:

_LABEL_SHAPE_MAP = {
    "power_in": "input",
    "power_out": "output",
}

def _label_shape(shape: str) -> str:
    return _LABEL_SHAPE_MAP.get(shape, shape)

Three lines plus a two-entry dictionary. Applied at three render points — global labels, hierarchical labels, and sheet pins — and the schematics parse clean. The mapping is semantically correct, too: a power input is an input in terms of signal direction. KiCad just doesn’t want the power-specific qualifier in the shape field.

Total time debugging: about twenty minutes. Time staring at the error message before realizing power_in was valid-but-not-here: most of those twenty minutes.

The General Pattern

This isn’t a KiCad-specific lesson. It’s a pattern that shows up anywhere a format or protocol reuses terms across different contexts:

HTTP has Content-Type in both request and response headers, but the valid values and their implications differ. A multipart/form-data content type on a request triggers entirely different parsing behavior than on a response — same header name, different semantics.

SQL uses NULL in column definitions (constraint), WHERE clauses (comparison), and INSERT statements (value), with subtly different behavior in each. NULL = NULL is false. NULL IS NULL is true. Same word, different evaluation rules depending on context.

CSS has auto as a valid value for margin, width, height, overflow, z-index, and a dozen other properties — meaning completely different things in each. margin: auto centers a block element. height: auto means “figure it out from content.” overflow: auto means “add scrollbars if needed.” One word, at least four distinct algorithms.

The deeper pattern: when two subsystems grow independently but share an ancestor vocabulary, they’ll accumulate divergent valid sets that partially overlap. The overlap is what makes it dangerous. If the sets were completely disjoint, you’d catch the mismatch immediately. It’s the partial overlap that lets most values pass through silently while a few specific ones detonate.

What Would Actually Prevent This

A type system would catch it — if the format had one. Separate enums for PinElectricalType and LabelShape would make the mismatch a compile-time error. But KiCad’s s-expression format is untyped text. There’s no schema validator that distinguishes these contexts. The parser is the only enforcement, and it only runs when you try to load the file.

For programmatic generation, the defense is straightforward: never pass values across context boundaries without explicit mapping, even when the values look compatible. Especially when they look compatible. The fact that input works in both places is not evidence that all values work in both places — it’s a coincidence that masks a boundary.

Trust the type, not the word. And when there is no type, build one yourself — even if it’s just a three-line dictionary.