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.
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"]
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):
#!/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
.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
=== 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:
- Why
TL074amplifier pins useu.p[n]while power pins useu["V+"]: Look at[Amplifier_Operational:TL074]: all four op-amp units share identical pin names ('+','-', and empty''). So, if you wroteu["+"], 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 asu.p[3],u.p[1],u.p[2]—targets physical SOIC-14 package pin numbers unambiguously, whereasu["V+"](pin 4) andu["V-"](pin 11) are unique and readable by name. - Why
AudioJack2_SwitchTpins are indexed asj_in["S"],j_in["T"], andj_in["TN"]: Look at[Connector_Audio:AudioJack2_SwitchT]:pin S ('', PASSIVE), pin T ('', PASSIVE), pin TN ('', PASSIVE). On this symbol, the lettersS(Sleeve),T(Tip), andTN(Switched Tip Normal Contact (TN)) are the actual pin numbers (pin.num) rather than integers1, 2, 3! - Why
C_Polarizedbulk caps usec_bulk[1]andc_bulk[2]: Look at[Device:C_Polarized]:pin 1 ('', PASSIVE), pin 2 ('', PASSIVE)—where pin1is the positive anode and pin2is the negative cathode.
Step 3. Interactive Component, Pinout & LCSC Explorer
Next, we queried live JLCPCB stock and feeder classes using jlcpcb_search.py:
.venv/bin/python .agents/skills/skidl/scripts/jlcpcb_search.py --in-stock-only "100k 0603" "TL074H SOIC-14"
part.fields["LCSC"], Never part.LCSCWhich 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
| Value / MPN | KiCad 10 Symbol | SKiDL Pin Indexing | Footprint | LCSC & Class |
|---|---|---|---|---|
TL074HTL074HIDR |
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 |
1N5819WSSchottky 40V 1A |
Device:D_Schottky |
d["K"] (pin 1), d["A"] (pin 2) |
Diode_SMD:D_SOD-323 |
C191023 · Basic |
PESD12VL1BA12V Bidir TVS |
Device:D_TVS |
in_jack & d_tvs & gnd (pins 1/A1, 2/A2) |
Diode_SMD:D_SOD-323 |
C38558 · Extended |
100nFCC0603KRX7R9BB104 |
Device:C |
vcc & c_dec & gnd (pins 1, 2) |
Capacitor_SMD:C_0603_1608Metric |
C14663 · Basic |
100K0603WAF1003T5E |
Device:R |
in_jack & r_pd & gnd (pins 1, 2) |
Resistor_SMD:R_0603_1608Metric |
C25803 · Basic |
1K0603WAF1001T5E |
Device:R |
in_jack & r_in & in_sig (pins 1, 2) |
Resistor_SMD:R_0603_1608Metric |
C21190 · Basic |
470R0603WAF4700T5E |
Device:R |
op_out & r_out & jack_tip (pins 1, 2) |
Resistor_SMD:R_0603_1608Metric |
C23179 · Basic |
WQP-PJ398SMThonkiconn 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) |
10uFECEA1EKS100 |
Device:C_Polarized |
c_bulk[1] (+), c_bulk[2] (-) |
Capacitor_THT:CP_Radial_D5.0mm_P2.00mm |
Manual THT (DNP for SMT) |
EURORACK_PWR_10PIN2x5 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:
#!/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:
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: 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!