Lesson 0002 · Stage 2 (Discovery & Growing the Constants Block)

Programmatic Part Discovery & Adding FP_* / LCSC_* Constants

Write a standalone Python discovery script to inspect exact KiCad 10 symbol pinouts, verify .pretty footprint files on disk, query live JLCPCB/LCSC stock, and grow our main circuit script with verified FP_* and LCSC_* constants.

Script Growth: Header → + 14 Footprint & LCSC Constants ✓ ERC: 0 Errors, 0 Warnings ✓ Ruff: Clean

How Do We Know Which Symbol, Pin Number, Footprint, and LCSC Part to Write?

In traditional GUI schematic capture, you hunt through nested menus to find a symbol, drop it on a sheet, and cross your fingers that its pin numbers match your footprint when you finally open the PCB layout editor.

But, when we're designing circuits from scratch in Python with SKiDL, we don't have to guess symbol names or pin numbers. Instead, we can treat KiCad 10's symbol libraries and the JLCPCB parts catalogue like any other API—write a small Python inspection script to query the exact pinouts and verify that the .kicad_mod footprint files actually exist on disk, then copy those verified constants straight into our growing circuit script.

flowchart TD
  Q1["1. Symbol Search<br/>search('TL074') / search('AudioJack2_SwitchT')"] --> Q2["2. Pinout Inspection<br/>Part(lib, sym, dest=TEMPLATE).pins"]
  Q2 --> Q3["3. Footprint Verification<br/>Check <Lib>.pretty/<Footprint>.kicad_mod"]
  Q3 --> Q4["4. Live JLCPCB Catalog Query<br/>jlcpcb_search.py --in-stock-only"]
  Q4 --> Q5["5. Grow buffered_multiple.py<br/>Add FP_* & LCSC_* constants + part.fields"]
The four-step discovery pipeline that feeds verified constants into Stage 2 of our growing circuit script.

Step 1. Write the Symbol, Pinout & Footprint Discovery Script

Starting with the exact same 34-line pre-import environment header we wrote in Lesson 0001, we can load any KiCad 10 symbol as a Template Part (dest=TEMPLATE) and iterate over part_t.pins to print every pin's exact .num, .name, and electrical function (PASSIVE, INPUT, OUTPUT, PWRIN).

Here is the complete, runnable 131-line discovery script (tutorial/apps/0002-programmatic-component-discovery/discover_parts.py):

Complete Discovery Script · tutorial/apps/0002-programmatic-component-discovery/discover_parts.py
#!/usr/bin/env python3
"""
Lesson 0002: Programmatic Component, Pinout, Footprint & JLCPCB Discovery.

Demonstrates how to inspect KiCad 10 symbols, pin numbers/names/types, `.pretty`
footprints, and custom `part.fields` BOM metadata programmatically in Python
for the parts used in `buffered_multiple.py`.
"""

import os
from pathlib import Path

# Keep generated .erc, .log, and _sklib.py artifacts inside this lesson's app directory
os.chdir(Path(__file__).resolve().parent)

# Configure writable cache directories and KiCad 10 library paths BEFORE importing skidl
os.environ.setdefault("XDG_DATA_HOME", "/tmp/xdg_data")
os.environ.setdefault("MPLCONFIGDIR", "/tmp/mpl_config")
if os.path.isdir("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"):
    os.environ.setdefault(
        "KICAD10_SYMBOL_DIR",
        "/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols",
    )
    os.environ.setdefault(
        "KICAD_SYMBOL_DIR",
        "/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols",
    )
if os.path.isdir("/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints"):
    os.environ.setdefault(
        "KICAD10_FOOTPRINT_DIR",
        "/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints",
    )

from skidl import ERC, POWER, TEMPLATE, Net, Part  # noqa: E402

# Core symbols we need for our Eurorack Buffered Multiple
BUFFERED_MULT_SYMBOLS = [
    ("Connector_Generic", "Conn_02x05_Odd_Even", "10-pin Eurorack IDC power header"),
    ("Device", "D_Schottky", "1N5819WS reverse-polarity protection diode"),
    ("Device", "C_Polarized", "10uF radial electrolytic bulk capacitor"),
    ("Amplifier_Operational", "TL074", "TL074H quad JFET/CMOS op-amp"),
    ("Connector_Audio", "AudioJack2_SwitchT", "WQP-PJ398SM switched 3.5mm mono jack"),
    ("Device", "D_TVS", "PESD12VL1BA bidirectional ESD/TVS protection diode"),
    ("Device", "R", "0603 SMD resistor (100K, 1K, 470R)"),
    ("Device", "C", "0603 SMD ceramic decoupling capacitor (100nF)"),
]

# Target `<Library>:<FootprintName>` strings
BUFFERED_MULT_FOOTPRINTS = [
    "Package_SO:SOIC-14_3.9x8.7mm_P1.27mm",
    "Resistor_SMD:R_0603_1608Metric",
    "Capacitor_SMD:C_0603_1608Metric",
    "Capacitor_THT:CP_Radial_D5.0mm_P2.00mm",
    "Diode_SMD:D_SOD-323",
    "Connector_Audio:Jack_3.5mm_QingPu_WQP-PJ398SM_Vertical_CircularHoles",
    "Connector_IDC:IDC-Header_2x05_P2.54mm_Vertical",
]


def inspect_symbols():
    """Load each symbol as a TEMPLATE and print its exact pin numbers, names, and types."""
    print("=== 1. KiCad 10 Symbol & Pinout Inspection ===")
    for lib_name, sym_name, role in BUFFERED_MULT_SYMBOLS:
        part_t = Part(lib_name, sym_name, dest=TEMPLATE)
        sorted_pins = sorted(
            part_t.pins,
            key=lambda pin: (
                (0, int(pin.num)) if str(pin.num).isdigit() else (1, str(pin.num))
            ),
        )
        pins_summary = ", ".join(
            f"pin {p.num} ('{p.name}', {p.func.name})" for p in sorted_pins
        )
        print(f"\n[{lib_name}:{sym_name}] — {role}")
        print(f"  Pin count: {len(part_t.pins)}")
        print(f"  Pins     : {pins_summary}")


def verify_footprints():
    """Verify every `<Lib>:<Footprint>` resolves to a `.kicad_mod` file on disk."""
    print("\n=== 2. KiCad 10 Footprint Verification ===")
    fp_root = Path(os.environ.get("KICAD10_FOOTPRINT_DIR", ""))
    for fp_id in BUFFERED_MULT_FOOTPRINTS:
        lib_nick, fp_name = fp_id.split(":", 1)
        mod_path = fp_root / f"{lib_nick}.pretty" / f"{fp_name}.kicad_mod"
        status = "FOUND" if mod_path.is_file() else "MISSING"
        print(f"  [{status}] {fp_id} -> {mod_path.name}")


def verify_template_fields_and_erc():
    """Demonstrate why custom BOM metadata belongs in `part.fields` and run ERC."""
    print("\n=== 3. Template Metadata (`part.fields`) & ERC Smoke Check ===")
    r_t = Part(
        "Device",
        "R",
        dest=TEMPLATE,
        value="100K",
        footprint="Resistor_SMD:R_0603_1608Metric",
    )
    # Correct pattern: store catalog keys in `part.fields` so clones and BOM exports inherit them
    r_t.fields["LCSC"] = "C25803"
    r_t.fields["MPN"] = "0603WAF1003T5E"
    r_t.fields["JLCPCB_Class"] = "Basic"

    r_pd = r_t(tag="r_pd_test")
    d_tvs = Part(
        "Device",
        "D_TVS",
        value="PESD12VL1BA",
        footprint="Diode_SMD:D_SOD-323",
        tag="d_tvs_test",
    )
    d_tvs.fields["LCSC"] = "C38558"
    d_tvs.fields["MPN"] = "PESD12VL1BA,115"
    d_tvs.fields["JLCPCB_Class"] = "Extended"
    print(f"  Instantiated {r_pd.ref} ({r_pd.value}) with fields: {r_pd.fields}")
    print(f"  Instantiated {d_tvs.ref} ({d_tvs.value}) with fields: {d_tvs.fields}")

    in_jack = Net("CH1_JACK_IN")
    gnd = Net("GND")
    in_jack.drive = POWER
    gnd.drive = POWER
    in_jack & (d_tvs | r_pd) & gnd

    ERC()


if __name__ == "__main__":
    inspect_symbols()
    verify_footprints()
    verify_template_fields_and_erc()

Run the Discovery Script & Read Its Output

Terminal · Run discover_parts.py and lint with ruff
.venv/bin/python tutorial/apps/0002-programmatic-component-discovery/discover_parts.py
ruff check --fix tutorial/apps/0002-programmatic-component-discovery/*.py
ruff format tutorial/apps/0002-programmatic-component-discovery/*.py
Verified Output · discover_parts.py
=== 1. KiCad 10 Symbol & Pinout Inspection ===

[Connector_Generic:Conn_02x05_Odd_Even] — 10-pin Eurorack IDC power header
  Pin count: 10
  Pins     : pin 1 ('Pin_1', PASSIVE), pin 2 ('Pin_2', PASSIVE), pin 3 ('Pin_3', PASSIVE), pin 4 ('Pin_4', PASSIVE), pin 5 ('Pin_5', PASSIVE), pin 6 ('Pin_6', PASSIVE), pin 7 ('Pin_7', PASSIVE), pin 8 ('Pin_8', PASSIVE), pin 9 ('Pin_9', PASSIVE), pin 10 ('Pin_10', PASSIVE)

[Device:D_Schottky] — 1N5819WS reverse-polarity protection diode
  Pin count: 2
  Pins     : pin 1 ('K', PASSIVE), pin 2 ('A', PASSIVE)

[Device:C_Polarized] — 10uF radial electrolytic bulk capacitor
  Pin count: 2
  Pins     : pin 1 ('', PASSIVE), pin 2 ('', PASSIVE)

[Amplifier_Operational:TL074] — TL074H quad JFET/CMOS op-amp
  Pin count: 14
  Pins     : pin 1 ('', OUTPUT), pin 2 ('-', INPUT), pin 3 ('+', INPUT), pin 4 ('V+', PWRIN), pin 5 ('+', INPUT), pin 6 ('-', INPUT), pin 7 ('', OUTPUT), pin 8 ('', OUTPUT), pin 9 ('-', INPUT), pin 10 ('+', INPUT), pin 11 ('V-', PWRIN), pin 12 ('+', INPUT), pin 13 ('-', INPUT), pin 14 ('', OUTPUT)

[Connector_Audio:AudioJack2_SwitchT] — WQP-PJ398SM switched 3.5mm mono jack
  Pin count: 3
  Pins     : pin S ('', PASSIVE), pin T ('', PASSIVE), pin TN ('', PASSIVE)

[Device:D_TVS] — PESD12VL1BA bidirectional ESD/TVS protection diode
  Pin count: 2
  Pins     : pin 1 ('A1', PASSIVE), pin 2 ('A2', PASSIVE)

[Device:R] — 0603 SMD resistor (100K, 1K, 470R)
  Pin count: 2
  Pins     : pin 1 ('', PASSIVE), pin 2 ('', PASSIVE)

[Device:C] — 0603 SMD ceramic decoupling capacitor (100nF)
  Pin count: 2
  Pins     : pin 1 ('', PASSIVE), pin 2 ('', PASSIVE)

=== 2. KiCad 10 Footprint Verification ===
  [FOUND] Package_SO:SOIC-14_3.9x8.7mm_P1.27mm -> SOIC-14_3.9x8.7mm_P1.27mm.kicad_mod
  [FOUND] Resistor_SMD:R_0603_1608Metric -> R_0603_1608Metric.kicad_mod
  [FOUND] Capacitor_SMD:C_0603_1608Metric -> C_0603_1608Metric.kicad_mod
  [FOUND] Capacitor_THT:CP_Radial_D5.0mm_P2.00mm -> CP_Radial_D5.0mm_P2.00mm.kicad_mod
  [FOUND] Diode_SMD:D_SOD-323 -> D_SOD-323.kicad_mod
  [FOUND] Connector_Audio:Jack_3.5mm_QingPu_WQP-PJ398SM_Vertical_CircularHoles -> Jack_3.5mm_QingPu_WQP-PJ398SM_Vertical_CircularHoles.kicad_mod
  [FOUND] Connector_IDC:IDC-Header_2x05_P2.54mm_Vertical -> IDC-Header_2x05_P2.54mm_Vertical.kicad_mod

=== 3. Template Metadata (`part.fields`) & ERC Smoke Check ===
  Instantiated R1 (100K) with fields: {'LCSC': 'C25803', 'MPN': '0603WAF1003T5E', 'JLCPCB_Class': 'Basic'}
  Instantiated D1 (PESD12VL1BA) with fields: {'LCSC': 'C38558', 'MPN': 'PESD12VL1BA,115', 'JLCPCB_Class': 'Extended'}
ERC INFO: No errors or warnings found while running ERC.

Step 2. Three Hardware Insights from the Discovery Output

Look closely at the pinout output printed above. Turns out, inspecting those eight symbols upfront reveals three critical details about how we need to index pins when we write our subcircuits in Lessons 0003 and 0004:

  1. Why TL074 amplifier pins use u.p[n] while power pins use u["V+"]: Look at [Amplifier_Operational:TL074]: all four op-amp units share identical pin names ('+', '-', and empty ''). So, if you wrote u["+"], SKiDL would dutifully match all four non-inverting inputs at once! Not quite what we want. Using the Numeric Pin Accessor (part.p[n])—such as u.p[3], u.p[1], u.p[2]—targets physical SOIC-14 package pin numbers unambiguously, whereas u["V+"] (pin 4) and u["V-"] (pin 11) are unique and readable by name.
  2. Why AudioJack2_SwitchT pins are indexed as j_in["S"], j_in["T"], and j_in["TN"]: Look at [Connector_Audio:AudioJack2_SwitchT]: pin S ('', PASSIVE), pin T ('', PASSIVE), pin TN ('', PASSIVE). On this symbol, the letters S (Sleeve), T (Tip), and TN (Switched Tip Normal Contact (TN)) are the actual pin numbers (pin.num) rather than integers 1, 2, 3!
  3. Why C_Polarized bulk caps use c_bulk[1] and c_bulk[2]: Look at [Device:C_Polarized]: pin 1 ('', PASSIVE), pin 2 ('', PASSIVE)—where pin 1 is the positive anode and pin 2 is the negative cathode.

Step 3. Interactive Component, Pinout & LCSC Explorer

Next, we queried live JLCPCB stock and feeder classes using jlcpcb_search.py:

Terminal · Query live JLCPCB/LCSC stock and feeder class
.venv/bin/python .agents/skills/skidl/scripts/jlcpcb_search.py --in-stock-only "100k 0603" "TL074H SOIC-14"
Critical Gotcha: Always Use part.fields["LCSC"], Never part.LCSC

Which leads me to an interesting realisation about how SKiDL stores part metadata. If you write r_pd.LCSC = "C25803" or pass LCSC="C25803" to Part(...), Python happily creates a plain instance attribute—and then generate_xml() and generate_netlist() silently ignore it! The reason this happens is that SkidlBaseObject.__setattr__ only redirects an attribute write into part.fields if that key already exists in part.fields. So, always assign custom catalogue metadata straight into the Part Fields Dictionary (part.fields): r_pd.fields["LCSC"] = "C25803". Better yet, any fields set on a Template Part (dest=TEMPLATE) are automatically copied to every part cloned from that template.

Use the interactive filter below to explore all 10 component types we discovered, comparing their KiCad 10 symbol pinouts, footprints, and JLCPCB Basic Part vs. JLCPCB Extended Part assembly classes:

Buffered Multiple Component & Pinout Explorer

Verified KiCad 10 symbol pins, footprints, and LCSC numbers
Value / MPN KiCad 10 Symbol SKiDL Pin Indexing Footprint LCSC & Class
TL074H
TL074HIDR
Amplifier_Operational:TL074 u["V+"] (4), u["V-"] (11)
u.p[1..3], u.p[5..10], u.p[12..14]
Package_SO:SOIC-14_3.9x8.7mm_P1.27mm C2878713 · Extended
1N5819WS
Schottky 40V 1A
Device:D_Schottky d["K"] (pin 1), d["A"] (pin 2) Diode_SMD:D_SOD-323 C191023 · Basic
PESD12VL1BA
12V Bidir TVS
Device:D_TVS in_jack & d_tvs & gnd (pins 1/A1, 2/A2) Diode_SMD:D_SOD-323 C38558 · Extended
100nF
CC0603KRX7R9BB104
Device:C vcc & c_dec & gnd (pins 1, 2) Capacitor_SMD:C_0603_1608Metric C14663 · Basic
100K
0603WAF1003T5E
Device:R in_jack & r_pd & gnd (pins 1, 2) Resistor_SMD:R_0603_1608Metric C25803 · Basic
1K
0603WAF1001T5E
Device:R in_jack & r_in & in_sig (pins 1, 2) Resistor_SMD:R_0603_1608Metric C21190 · Basic
470R
0603WAF4700T5E
Device:R op_out & r_out & jack_tip (pins 1, 2) Resistor_SMD:R_0603_1608Metric C23179 · Basic
WQP-PJ398SM
Thonkiconn Jack
Connector_Audio:AudioJack2_SwitchT j["S"], j["T"], j["TN"] Connector_Audio:Jack_3.5mm_QingPu_WQP-PJ398SM_Vertical_CircularHoles Manual THT (DNP for SMT)
10uF
ECEA1EKS100
Device:C_Polarized c_bulk[1] (+), c_bulk[2] (-) Capacitor_THT:CP_Radial_D5.0mm_P2.00mm Manual THT (DNP for SMT)
EURORACK_PWR_10PIN
2x5 Shrouded IDC
Connector_Generic:Conn_02x05_Odd_Even j_pwr[1, 2], j_pwr[3..8], j_pwr[9, 10] Connector_IDC:IDC-Header_2x05_P2.54mm_Vertical Manual THT (DNP for SMT)

Step 4. Growing buffered_multiple.py (Stage 2: Header + Constants)

Now, let's take the pre-import header we wrote in Lesson 0001 and grow our main circuit script by adding the 7 footprint constants (FP_*) and 7 LCSC catalogue constants (LCSC_*) we just verified. In Lesson 0003, we will add @subcircuit def eurorack_power(vcc, vee, gnd): directly below line 68:

Growing buffered_multiple.py · Stage 2 (Lines 1–68: Environment Header + Footprint & LCSC Constants)
#!/usr/bin/env python3
import builtins
import csv
import os
from pathlib import Path

# Keep generated .erc, .log, .net, .xml, .csv, and _sklib.py files inside this script's directory
os.chdir(Path(__file__).resolve().parent)

# Configure writable cache directories and KiCad 10 library paths BEFORE importing skidl
os.environ.setdefault("XDG_DATA_HOME", "/tmp/xdg_data")
os.environ.setdefault("MPLCONFIGDIR", "/tmp/mpl_config")
if os.path.isdir("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"):
    os.environ.setdefault(
        "KICAD10_SYMBOL_DIR",
        "/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols",
    )
    os.environ.setdefault(
        "KICAD_SYMBOL_DIR",
        "/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols",
    )
if os.path.isdir("/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints"):
    os.environ.setdefault(
        "KICAD10_FOOTPRINT_DIR",
        "/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints",
    )

from skidl import (  # noqa: E402
    ERC,
    POWER,
    TEMPLATE,
    Net,
    NetClass,
    Part,
    generate_netlist,
    generate_xml,
    subcircuit,
)

# NEW IN STAGE 1: Verified KiCad 10 Footprints (SMD passives & ICs, THT jacks, power header & bulk caps)
FP_OPAMP_SOIC14 = "Package_SO:SOIC-14_3.9x8.7mm_P1.27mm"
FP_R_0603 = "Resistor_SMD:R_0603_1608Metric"
FP_C_0603 = "Capacitor_SMD:C_0603_1608Metric"
FP_C_ELEC = "Capacitor_THT:CP_Radial_D5.0mm_P2.00mm"
FP_DIODE_SOD323 = "Diode_SMD:D_SOD-323"
FP_JACK_EURORACK = (
    "Connector_Audio:Jack_3.5mm_QingPu_WQP-PJ398SM_Vertical_CircularHoles"
)
FP_PWR_HEADER_10P = "Connector_IDC:IDC-Header_2x05_P2.54mm_Vertical"

# NEW IN STAGE 1: Verified JLCPCB / LCSC Catalog Part Numbers (SMT assembly)
LCSC_C_100NF_0603 = "C14663"  # YAGEO CC0603KRX7R9BB104 (50V X7R, Basic Part)
LCSC_R_100K_0603 = "C25803"  # UNI-ROYAL 0603WAF1003T5E (100k 1%, Basic Part)
LCSC_R_1K_0603 = "C21190"  # UNI-ROYAL 0603WAF1001T5E (1k 1%, Basic Part)
LCSC_R_470R_0603 = "C23179"  # UNI-ROYAL 0603WAF4700T5E (470R 1%, Basic Part)
LCSC_D_1N5819WS_SOD323 = "C191023"  # CJ 1N5819WS (40V 1A Schottky SOD-323, Basic Part)
LCSC_D_TVS_PESD12VL1BA_SOD323 = "C38558"  # Nexperia PESD12VL1BA,115 (12V Bidir TVS SOD-323, Extended Part)
LCSC_U_TL074H_SOIC14 = "C2878713"  # Texas Instruments TL074HIDR (SOIC-14, Extended Part)

Retrieval Practice: Assign Template BOM Fields & Wire Pin Indices

Fill in the four blanks below to store the LCSC part number and JLCPCB class in the template's fields dictionary, connect +12V to the TL074 positive supply pin by name, and wire the input signal to Unit A's non-inverting input (pin 3) using SKiDL's numeric pin accessor:

Interactive Python Exercise (Prism keep-markup)
c_t = Part("Device", "C", dest=TEMPLATE, footprint="Capacitor_SMD:C_0603_1608Metric")
c_t.["LCSC"] = "C14663"
c_t.fields[""] = "Basic"

u = Part("Amplifier_Operational", "TL074", value="TL074H", footprint="Package_SO:SOIC-14_3.9x8.7mm_P1.27mm")
vcc += u[""]
in_sig += u.[3]

Knowledge Check

1. Why must we index TL074 amplifier pins with u.p[3] instead of u["+"]?

2. Why must LCSC catalog numbers be stored in part.fields["LCSC"] rather than part.LCSC?

Primary Sources & Next Steps

Primary Sources: KiCad 10 Symbol, Footprint & JLCPCB Lookup Reference · SKiDL API & Pin Indexing Cheat Sheet · TI TL074H Datasheet.
Ask the Agent: Want to try searching for another Eurorack component (like an SSI2164 VCA, LM13700 OTA, or potentiometer footprint), or ready for Lesson 0003 where we grow Stage 2 into a working Eurorack power subcircuit? Just ask in chat!