Skip to main content

Python helper for KiCad symbol S-expressions

Project description

kicad-sym

Python helper for KiCad symbol S-expressions.

The library is intentionally low-level:

  • forms are plain Python list objects
  • quoted strings are plain Python str
  • bare atoms are kicad_sym.Sym
  • integers and floats stay native

The core workflow is:

  1. read a .kicad_sym file
  2. modify the tree in Python
  3. write the whole file back out

There is no patch DSL and no heavy semantic object model.

Install

uv add kicad-sym

One-off script without editing your project dependencies:

uv run --with kicad-sym --no-project python - <<'PY'
import kicad_sym as ks

print(ks.dumps(ks.library(ks.symbol("Demo"))))
PY

Basic Usage

uv run --with kicad-sym --no-project python - <<'PY'
import kicad_sym as ks

lib = ks.load("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/Device.kicad_sym")
symbol = ks.get_symbol(lib, "R")

print(ks.symbol_name(symbol))
print(ks.nested_symbol_names(symbol))
print(ks.properties(symbol).get("Description"))
print(symbol)
PY

Parsed and built forms print as KiCad-style S-expressions, so print(node) and REPL inspection are useful for quick debugging of intermediate states.

Read A Multi-Symbol Library

Use symbol_names() to inspect the top-level symbols, then get_symbol() to pick the one you want.

uv run --with kicad-sym --no-project python - <<'PY'
import kicad_sym as ks

lib = ks.load("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/Device.kicad_sym")

print(ks.symbol_names(lib)[:5])

resistor = ks.get_symbol(lib, "R")
capacitor = ks.get_symbol(lib, "C")

print(ks.symbol_name(resistor), ks.properties(resistor).get("Description"))
print(ks.symbol_name(capacitor), ks.properties(capacitor).get("Description"))
PY

For editing, the simplest workflow is still to target one top-level symbol at a time, modify it in memory, and write the whole library back out.

Query-Like Views In Python

The main pcb kq use cases translate directly into ordinary Python traversals. The library handles tree access; Python handles filtering, sorting, and shaping the result.

Metadata View

uv run --with kicad-sym --no-project python - <<'PY'
import kicad_sym as ks

lib = ks.load("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/Interface_USB.kicad_sym")
sym = ks.get_symbol(lib, "ADUM4160")

print(ks.properties(sym))
PY

Electrical Signature

uv run --with kicad-sym --no-project python - <<'PY'
import kicad_sym as ks

def pin_view(pin):
    return {
        "name": ks.child(pin, "name")[1],
        "number": ks.child(pin, "number")[1],
        "electrical": str(pin[1]),
        "graphic": str(pin[2]),
    }

lib = ks.load("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/Interface_USB.kicad_sym")
sym = ks.get_symbol(lib, "ADUM4160")

rows = [pin_view(pin) for pin in ks.pins(sym)]
rows.sort(key=lambda row: row["number"])
print(rows)
PY

Exact Unit In A Multi-Unit Symbol

uv run --with kicad-sym --no-project python - <<'PY'
import kicad_sym as ks

def pin_view(pin):
    return {
        "name": ks.child(pin, "name")[1],
        "number": ks.child(pin, "number")[1],
        "electrical": str(pin[1]),
        "graphic": str(pin[2]),
    }

lib = ks.load("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/74xx.kicad_sym")
sym = ks.get_symbol(lib, "74LS00")
gate = ks.get_nested_symbol(sym, "74LS00_1_1")
power = ks.get_nested_symbol(sym, "74LS00_5_0")

print(ks.nested_symbol_names(sym)[:6])
print([pin_view(pin) for pin in ks.pins(gate)])
print([pin_view(pin) for pin in ks.pins(power)])
PY

Art And Geometry View

uv run --with kicad-sym --no-project python - <<'PY'
import kicad_sym as ks

lib = ks.load("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/Interface_USB.kicad_sym")
sym = ks.get_symbol(lib, "ADUM4160")

text_art = ks.get_nested_symbol(sym, "ADUM4160_0_0")
body_art = ks.get_nested_symbol(sym, "ADUM4160_0_1")
pin_unit = ks.get_nested_symbol(sym, "ADUM4160_1_1")

labels = [node[1] for node in ks.walk(text_art, "text")]
rect = next(ks.walk(body_art, "rectangle"))
start = tuple(ks.child(rect, "start")[1:3])
end = tuple(ks.child(rect, "end")[1:3])
placements = [
    {
        "name": ks.child(pin, "name")[1],
        "number": ks.child(pin, "number")[1],
        "at": tuple(ks.child(pin, "at")[1:4]),
    }
    for pin in ks.pins(pin_unit)
]

print(labels)
print({"start": start, "end": end})
print(placements[:4])
PY

Explore Artwork In An Existing Symbol

Artwork-heavy symbols are easiest to explore by:

  1. loading the top-level symbol
  2. listing its nested unit/style symbols
  3. picking the art-bearing nested symbol
  4. walking only the draw nodes you care about

ADUM4160 is a good example because its body art and pin placement live in different nested symbols.

uv run --with kicad-sym --no-project python - <<'PY'
from pathlib import Path
import kicad_sym as ks

DRAW = {"polyline", "rectangle", "circle", "arc", "bezier", "text", "text_box"}
root = Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols")

lib = ks.load(root / "Interface_USB.kicad_sym")
sym = ks.get_symbol(lib, "ADUM4160")

print(ks.nested_symbol_names(sym))

body_art = ks.get_nested_symbol(sym, "ADUM4160_0_1")
draw_nodes = [node for node in ks.walk(body_art) if ks.head(node) in DRAW]

print("draw nodes:", len(draw_nodes))
for node in draw_nodes[:3]:
    print()
    print(node)
PY

Because forms print as KiCad-style S-expressions, print(node) is usually enough to inspect intermediate artwork state while you are iterating.

Write A Multi-Symbol Library

Build or load the top-level symbols you want, keep them in one library(...) root, then save the entire file.

uv run --with kicad-sym --no-project python - <<'PY'
import kicad_sym as ks

lib = ks.library(
    ks.symbol(
        "R_Custom",
        ks.property("Reference", "R"),
        ks.property("Value", "R_Custom"),
    ),
    ks.symbol(
        "C_Custom",
        ks.property("Reference", "C"),
        ks.property("Value", "C_Custom"),
    ),
)

resistor = ks.get_symbol(lib, "R_Custom")
ks.set_property(resistor, "Description", "Example resistor in a multi-symbol library")

lib.append(
    ks.symbol(
        "TP_Custom",
        ks.property("Reference", "TP"),
        ks.property("Value", "TP_Custom"),
    )
)

path = "/tmp/custom-passives.kicad_sym"
ks.save(path, lib)
print(path)
print(ks.symbol_names(lib))
PY

Modify Artwork In An Existing Symbol

The usual pattern is:

  1. clone the symbol you want to modify
  2. rename the top-level symbol and any nested units you touched
  3. grab the nested art symbol
  4. replace or append draw nodes directly
uv run --with kicad-sym --no-project python - <<'PY'
from pathlib import Path
import kicad_sym as ks

root = Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols")
lib = ks.load(root / "Interface_USB.kicad_sym")
sym = ks.clone(ks.get_symbol(lib, "ADUM4160"))

sym[1] = "ADUM4160_Debug"
for nested in ks.nested_symbols(sym):
    if isinstance(nested[1], str) and nested[1].startswith("ADUM4160_"):
        nested[1] = nested[1].replace("ADUM4160", "ADUM4160_Debug", 1)

body_art = ks.get_nested_symbol(sym, "ADUM4160_Debug_0_1")

def replace_child(node, kind, new_child):
    for i, child in enumerate(node[1:], start=1):
        if isinstance(child, list) and ks.head(child) == kind:
            node[i] = new_child
            return
    node.append(new_child)

rect = next(ks.walk(body_art, "rectangle"))
replace_child(rect, "stroke", ks.stroke(0.508, stroke_type="default"))
replace_child(rect, "fill", ks.fill("background"))

body_art.append(
    ks.text(
        "DBG",
        at=(0, -9.5, 0),
        effects=ks.effects(font=ks.font(1.0, 1.0)),
    )
)
body_art.append(
    ks.circle(
        (0, 0),
        2.0,
        stroke=ks.stroke(0.254, stroke_type="default"),
        fill=ks.fill("none"),
    )
)

out = ks.library(sym)
path = Path("/tmp/ADUM4160_Debug.kicad_sym")
ks.save(path, out)
print(path)
PY

Update A Property In Place

uv run --with kicad-sym --no-project python - <<'PY'
import kicad_sym as ks

lib = ks.load("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/Device.kicad_sym")
symbol = ks.clone(ks.get_symbol(lib, "R"))

ks.set_property(symbol, "Datasheet", "https://example.com/resistor")
ks.set_property(symbol, "Description", "Resistor with updated metadata")
ks.set_property(symbol, "Manufacturer", "Example Corp", hidden=True)

out = ks.library(symbol)
path = "/tmp/R_customized.kicad_sym"
ks.save(path, out)
print(path)
PY

Build A Minimal Single-Symbol Library

uv run --with kicad-sym --no-project python - <<'PY'
import kicad_sym as ks

sym = ks.symbol(
    "TestPoint_Pad",
    ks.form("pin_numbers", ks.form("hide", ks.yesno(True))),
    ks.form("pin_names", ks.form("offset", 0)),
    ks.form("exclude_from_sim", ks.yesno(False)),
    ks.form("in_bom", ks.yesno(True)),
    ks.form("on_board", ks.yesno(True)),
    ks.property("Reference", "TP", at=(0, 2.54, 0)),
    ks.property("Value", "TestPoint_Pad", at=(0, -2.54, 0)),
    ks.property("Footprint", "", at=(0, 0, 0), hidden=True),
    ks.property("Datasheet", "", at=(0, 0, 0), hidden=True),
    ks.property("Description", "One-pin test point", at=(0, 0, 0), hidden=True),
    ks.unit_symbol(
        "TestPoint_Pad",
        1,
        1,
        ks.circle((0, 0), 1.27),
        ks.pin("1", "TP", at=(-5.08, 0, 0)),
    ),
)

lib = ks.library(sym)
path = "/tmp/TestPoint_Pad.kicad_sym"
ks.save(path, lib)
print(path)
PY

Build Artwork From Scratch

The helpers cover the common draw forms directly: rectangle, polyline, circle, and text. For forms that do not have a dedicated helper yet, such as arc, just drop down to raw form(...).

uv run --with kicad-sym --no-project python - <<'PY'
from pathlib import Path
import kicad_sym as ks

badge = ks.symbol(
    "Artwork_Debug_Badge",
    ks.property("Reference", "SYM", hidden=True),
    ks.property("Value", "Artwork_Debug_Badge", hidden=True),
    ks.property("Footprint", "", hidden=True),
    ks.property("Datasheet", "", hidden=True),
    ks.property("Description", "Artwork example", hidden=True),
    ks.unit_symbol(
        "Artwork_Debug_Badge",
        0,
        1,
        ks.circle(
            (0, 0),
            10,
            stroke=ks.stroke(0.4, stroke_type="default"),
            fill=ks.fill("none"),
        ),
        ks.circle(
            (0, 0),
            7,
            stroke=ks.stroke(0.25, stroke_type="default"),
            fill=ks.fill("background"),
        ),
        ks.form(
            "arc",
            ks.form("start", -6, 0),
            ks.form("mid", 0, 6),
            ks.form("end", 6, 0),
            ks.stroke(0.35, stroke_type="default"),
            ks.fill("none"),
        ),
        ks.polyline(
            [(-2.5, 4), (0.5, 0.5), (-1, 0.5), (2.5, -4), (0, -0.5), (1.2, -0.5)],
            stroke=ks.stroke(0.35, stroke_type="default"),
            fill=ks.fill("none"),
        ),
        ks.text(
            "DBG",
            at=(0, -8.6, 0),
            effects=ks.effects(font=ks.font(0.9, 0.9)),
        ),
    ),
)

out = ks.library(badge)
path = Path("/tmp/Artwork_Debug_Badge.kicad_sym")
ks.save(path, out)
print(path)
PY

Build A Multi-Unit Symbol

Nested KiCad unit/style symbols are direct child (symbol ...) nodes under the top-level symbol, so multi-unit symbols fit the library naturally.

uv run --with kicad-sym --no-project python - <<'PY'
import kicad_sym as ks

dual = ks.symbol(
    "Demo_Dual_Opamp",
    ks.form("pin_names", ks.form("offset", 0.508)),
    ks.form("exclude_from_sim", ks.yesno(False)),
    ks.form("in_bom", ks.yesno(True)),
    ks.form("on_board", ks.yesno(True)),
    ks.property("Reference", "U", at=(0, 6.35, 0)),
    ks.property("Value", "Demo_Dual_Opamp", at=(0, -6.35, 0)),
    ks.property("Footprint", "", at=(0, 0, 0), hidden=True),
    ks.property("Datasheet", "", at=(0, 0, 0), hidden=True),
    ks.property("Description", "Example multi-unit symbol", at=(0, 0, 0), hidden=True),
    ks.unit_symbol(
        "Demo_Dual_Opamp",
        1,
        1,
        ks.polyline([(-5.08, -3.81), (5.08, 0), (-5.08, 3.81), (-5.08, -3.81)]),
        ks.pin("1", "-", electrical="input", at=(-7.62, 2.54, 0)),
        ks.pin("2", "+", electrical="input", at=(-7.62, -2.54, 0)),
        ks.pin("3", "~", electrical="output", at=(7.62, 0, 180)),
    ),
    ks.unit_symbol(
        "Demo_Dual_Opamp",
        2,
        1,
        ks.polyline([(-5.08, -3.81), (5.08, 0), (-5.08, 3.81), (-5.08, -3.81)]),
        ks.pin("5", "+", electrical="input", at=(-7.62, -2.54, 0)),
        ks.pin("6", "-", electrical="input", at=(-7.62, 2.54, 0)),
        ks.pin("7", "~", electrical="output", at=(7.62, 0, 180)),
    ),
    ks.unit_symbol(
        "Demo_Dual_Opamp",
        3,
        0,
        ks.pin("4", "V-", electrical="power_in", at=(0, -7.62, 90)),
        ks.pin("8", "V+", electrical="power_in", at=(0, 7.62, 270)),
    ),
)

lib = ks.library(dual)
path = "/tmp/Demo_Dual_Opamp.kicad_sym"
ks.save(path, lib)
print(path)
print(ks.nested_symbol_names(dual))
PY

KiCad Symbol Shape

A .kicad_sym file is an S-expression tree rooted at (kicad_symbol_lib ...).

(kicad_symbol_lib
  (symbol "NAME"
    SYMBOL_METADATA...
    GRAPHICS...
    PINS...
    (symbol "NAME_0_1" ...)
    (symbol "NAME_1_1" ...)
  )
  (symbol "ANOTHER_NAME" ...)
)

In practice:

  • top-level symbol nodes are the library entries
  • nested symbol nodes are units or styles inside a parent symbol
  • metadata is usually property, in_bom, on_board, pin_names, pin_numbers, and sometimes extends
  • drawing is built from forms like rectangle, polyline, circle, arc, and text
  • many real KiCad symbols put graphics and pins in different nested symbols

kicad-sym keeps that structure visible instead of wrapping it in a heavier object model.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kicad_sym-0.4.0.tar.gz (11.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kicad_sym-0.4.0-py3-none-any.whl (10.6 kB view details)

Uploaded Python 3

File details

Details for the file kicad_sym-0.4.0.tar.gz.

File metadata

  • Download URL: kicad_sym-0.4.0.tar.gz
  • Upload date:
  • Size: 11.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for kicad_sym-0.4.0.tar.gz
Algorithm Hash digest
SHA256 418b1a87ac87a48b4b5ada25632f1e4a3c2c3c1a8b5bb0f5dc0dd05bdceedb5f
MD5 520fd775839b735e7c8e72db0cb8e9aa
BLAKE2b-256 88db4bc4e106611f207b0bb1f620eec6c2ac850a1e3c565996d7fb8b1ea9d9d2

See more details on using hashes here.

File details

Details for the file kicad_sym-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: kicad_sym-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 10.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for kicad_sym-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7cda3139cbb73506fd657a464bc2a6539e19cfba9c07ebebb0e7e37909cb734e
MD5 2c1f78db85e44ff8a761781d4968eb64
BLAKE2b-256 b85156fad960e618e1f5a8a89ae8a60fedb9f1af8654fbdee55825eb4139574d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page