pcbkit generates KiCad schematics programmatically. kiutils is the most complete Python library for reading and writing KiCad's s-expression file formats, and for the most part it does what it says. Two problems came up in our first spike that the documentation doesn't mention. Both produce files that look fine on the machine that generated them and break somewhere else.

1. You own the format version, not the library

Every .kicad_sch starts with a version token, a date code like 20231120 that identifies the file-format revision. KiCad reads it to decide whether to open the file directly, run it through format migration, or refuse it as made with a newer version of KiCad.

kiutils serializes whatever version token its object model happens to carry, and its release cycle is not synchronized with KiCad's. Build a schematic object from scratch and you can end up emitting a version token that doesn't match the structures you actually wrote: a new token over old-format s-expressions, or the other way around. Depending on the mismatch, KiCad either runs a silent migration pass and rewrites your file on first save, or refuses to open it.

You have to own the version token yourself. Pin it to the KiCad release you test against, and assert it in the test suite so a kiutils upgrade can't move it without you noticing.

generate.py
from kiutils.schematic import Schematic

KICAD_SCH_VERSION = "20231120"  # KiCad 8.0 schematic format — the release we test against

sch = Schematic.create_new()
sch.version = KICAD_SCH_VERSION

# ...build the schematic...

sch.to_file("out.kicad_sch")

# in the test suite:
assert Schematic.from_file("out.kicad_sch").version == KICAD_SCH_VERSION

Verify against the real tool as well, not just by round-tripping through kiutils. Running kicad-cli sch erc out.kicad_sch in CI catches mismatches that a parse/serialize cycle won't.

2. Symbols must be embedded in lib_symbols, or your file won't travel

Since KiCad 6, a schematic file is self-contained. Every symbol placed in it must also be defined inside the file, in the top-level lib_symbols section; the placed instances just reference those definitions by lib id (Device:C, MCU_Module:Arduino_Nano_v3.x).

kiutils models this section but does not populate it. If you append SchematicSymbol instances without also copying each symbol definition from the .kicad_sym library into schematic.libSymbols, the file will usually open fine on the machine that generated it, because the installed system libraries fill the gap. On anyone else's machine it shows missing-symbol placeholders. It passes local inspection, which is what makes it easy to ship broken.

embed_symbol.py
from kiutils.symbol import SymbolLib

def embed_symbol(sch, lib_path, lib_nickname, entry_name):
    """Copy a symbol definition from a .kicad_sym library into the
    schematic's lib_symbols section, keyed by its full lib id."""
    lib = SymbolLib.from_file(lib_path)
    sym = next(s for s in lib.symbols if s.entryName == entry_name)
    sym.libId = f"{lib_nickname}:{entry_name}"  # embedded defs carry the full id
    if sym.libId not in [s.libId for s in sch.libSymbols]:
        sch.libSymbols.append(sym)
    return sym

embed_symbol(sch, "/usr/share/kicad/symbols/Device.kicad_sym", "Device", "C")

Setting libId matters. Definitions inside a library file are keyed by bare entry name, but embedded in a schematic they have to carry the Library:Entry form; kiutils propagates the rename to the child unit sub-symbols when you assign the property. The dedup check matters too, since KiCad handles duplicate entries badly.

The acceptance test for both problems is the same. Open the generated file on a machine without your symbol libraries installed, or unset KICAD8_SYMBOL_DIR and run kicad-cli sch export svg. If every symbol renders, the file travels.

pcbkit is an AI-assisted KiCad schematic tool in development — join the waitlist.