Skip to main content

kicad-evaltor

A Python library for validating KiCad designs programmatically. Describe the properties a design is supposed to have — reference designators, values, connectivity, footprint assignment, ERC results, and whether the sheet is readable — and run them as a test suite.

Works against KiCad 10 and later. It does not need the KiCad IPC API server, which KiCad 10 removed.

CI PyPI Python License: MIT

Installation

pip install kicad-evaltor

What you need installed

Most checks need nothing but the file itself. The schematic reader parses .kicad_sch directly, so symbols, fields, wires, labels and text geometry all work with no KiCad present.

A kicad-cli binary is needed only for the checks that shell out:

Check Needs
sch.components.connected kicad-cli sch export netlist
sch.erc kicad-cli sch erc

Without it those checks report an error and the rest of the suite still runs. On Windows, KiCad is not put on PATH by default; pass kicad_cli_path="C:/Program Files/KiCad/10.0/bin/kicad-cli.exe" or let DesignContext find it.

Quick start

from kicad_evaltor import (
    ComponentValueCheck,
    DesignContext,
    ERCRunCheck,
    TestRunner,
    TextTextOverlapCheck,
)

with DesignContext(project_path="myproject.kicad_pro", headless=True) as ctx:
    report = TestRunner(
        [
            ComponentValueCheck(reference="R1", expected="10k"),
            ERCRunCheck(severity="error"),
            TextTextOverlapCheck(),
        ]
    ).run(ctx)

    print(report.summary())
    assert report.all_passed

A complete, runnable example lives in examples/simple_circuit.py, which runs the schematic checks against examples/demo_circuit/simple_circuit.kicad_sch. It exits non-zero if the design stops matching its documented expectations, so it doubles as a smoke test:

python examples/simple_circuit.py

examples/hierarchy.py does the same for a hierarchical design, examples/demo_circuit/hierarchy/: a root sheet with the buttons and status LED, and a shared sheet below it holding the power stage and MCU. Its checks find parts on either sheet by reference, follow nets across the sheet block's pins, and name each layout finding's sheet. KiCad agrees on the design: 41 parts across / and /shared/, and a clean ERC.

python examples/hierarchy.py

examples/visual.py does the same for the layout checks, against examples/demo_circuit/visual.kicad_sch. It runs every sch.layout.* and sch.noconnect.* check the registry knows and lists each finding with the properties and coordinates involved. It is a report, not a test: it exits 0 even when the checks find defects. It needs no KiCad at all, and takes any sheet as an argument:

python examples/visual.py                                 # human report
python examples/visual.py --json                          # the same failures as JSON
python examples/visual.py path/to/your.kicad_sch          # any other sheet

Checks

Schematic

Check ID Class Description
sch.component.exists ComponentExistsCheck A component exists, by reference or lib_id pattern
sch.component.value ComponentValueCheck A component's value matches, case-insensitively
sch.component.property ComponentPropertyCheck An arbitrary field on a component matches
sch.components.connected ComponentsConnectedCheck Two components share at least N nets
sch.footprint.assigned FootprintAssignedCheck Footprints are assigned, optionally allowing None
sch.symbol.in_library SymbolInLibraryCheck A symbol exists in the project libraries
sch.erc ERCRunCheck Runs ERC via kicad-cli and filters by severity

Hierarchical sheets

A KiCad schematic is a tree of files: the root draws (sheet ...) blocks, and each block links to another file. Every check above works across that whole tree, so ComponentExistsCheck(reference="U1") finds U1 whether it was drawn on the root or four levels down. A reference means one thing in a design; which sheet it happens to sit on is not part of the question.

Four checks are about the hierarchy itself. They read the files directly and skip cleanly when the context has no file-backed schematic.

Check ID Class Description
sch.sheet.file_missing SheetFileMissingCheck A Sheetfile resolves to a readable .kicad_sch
sch.sheet.cycle SheetCycleCheck The sheet links contain no cycle
sch.sheet.pin_mismatch SheetPinMismatchCheck Every sheet pin has a matching hierarchical label, and the reverse
sch.sheet.name_or_page_collision SheetNameOrPageCollisionCheck Two sibling sheets share a name, or two sheets share a page number

sch.sheet.file_missing distinguishes a Sheetfile that names nothing on disk from one that does not parse, because the two mean different things. Relative paths resolve against the project folder first, the way KiCad resolves them, then against the folder of the file holding the link — so a Sheetfile of ../shared/shared.kicad_sch works from a subdirectory.

sch.sheet.cycle is keyed on the path taken to reach a sheet, not on the files seen so far: KiCad legitimately instantiates one file under several sheet symbols, and only a path that re-enters its own ancestor is a cycle.

sch.sheet.name_or_page_collision reports a sheet with no page number only when the root shows the design ever tracked pages at all. A hand-written or generated schematic has no (sheet_instances ...), and reporting every sheet in one would be noise rather than a finding.

Selecting sheets

The layout checks each take an optional sheet parameter naming which sheet to look at: a sheet name, a Sheetfile stem, or a KiCad instance path when one file is instantiated twice. sheet="all" and the default both mean every sheet.

TextSymbolOverlapCheck(sheet="shared")  # one sheet
TextSymbolOverlapCheck(sheet="all")  # the default

Omitting it covers the whole tree. A flat design is a one-sheet tree, so nothing changes for one, while a hierarchical design would otherwise be checked only on its root — which typically holds nothing but sheet symbols, and would report a clean bill of health. Findings from a non-root sheet are qualified with its name the same way a symbol's own text is, so D1.Reference on the hierarchical demo's shared sheet reads as shared.D1.Reference. Root-sheet findings are unqualified, exactly as before.

A selector that matches nothing is reported as a skip listing the valid names, never as a pass: a typo that quietly checked nothing would look identical to a clean design.

Layout

These read each sheet's own geometry rather than the netlist. Text boxes are computed from a stroke font calibrated against a real KiCad renderer, so the verdicts match what you see in the viewer.

Check ID Class Description
sch.layout.text_text_overlap TextTextOverlapCheck Two text items overlap
sch.layout.text_symbol_overlap TextSymbolOverlapCheck Text sits on a symbol
sch.layout.symbol_symbol_overlap SymbolSymbolOverlapCheck Two symbol bodies share space
sch.layout.text_off_sheet TextOffSheetCheck Text falls outside the sheet boundary
sch.layout.text_wire_overlap TextWireOverlapCheck Text sits on a wire. Opt-in: KiCad masks wires behind text, so this is usually benign
sch.layout.symbol_wire_overlap SymbolWireOverlapCheck A wire runs across a symbol's artwork, or into a pin from the body side
sch.noconnect.floating NoConnectFloatingCheck A no-connect flag marks no pin, or a drawn pin is reached by nothing at all

Hidden items are never reported. A field marked (hide yes) is skipped, a pin marked (hide yes) is not counted as a pin left open, and a power symbol's name is treated as symbol artwork rather than as text.

A component's own text is measured against its own body rather than the full body-and-pins box, and only crossing the outline counts. KiCad places a reference directly above a body, which is where the topmost pin stub is too, so measuring pins would report the default placement of every part. A small symbol is mostly empty space, so a value that fits wholly inside one is left alone. Text straddling the outline, half in and half out, is reported — and so is text over another symbol's pins.

Every finding carries the properties behind each label, so a report says what is on the sheet rather than only which field collided. Value is one property among many: user-defined ones such as an LCSC part number are reported the same way.

The two text-on-anything checks answer a legibility question and leave a 0.2mm clearance between the pair: text that comes within it is unreadable even though nothing is drawn on top of anything. Set clearance=0 to report only what genuinely overlaps. The remaining checks answer "is this drawn on that", so they take no clearance, and take a margin if the measurement noise of sampled outlines and bare pin lines needs hiding.

{
  "first": "SW2.Value", "first_kind": "text", "first_properties": {"Value": "SW_Push"},
  "second": "PWR012", "second_kind": "symbol", "second_properties": {"Value": "GND"},
  "area": [256.54, 44.78, 259.08, 46.17], "width": 2.54, "height": 1.39
}

Hidden properties are left out, on both sides: a field marked (hide yes) is not drawn, so it is not part of what the sheet shows.

report.to_json() serialises the whole run, report.to_dict() and result.to_dict() give the same data as Python objects, and report.failures_json() returns just the failures, which is usually what a CI job wants to read.

Custom checks

A check is a class with a stable dotted id, registered so it can be looked up by name.

from dataclasses import dataclass

from kicad_evaltor import Check, CheckResult, TestStatus, register


@dataclass
class MyParams:
    min_count: int
    prefix: str


@register
class MyComponentCountCheck(Check):
    id = "custom.component.count"
    name = "Component Count"
    description = "Ensures a minimum number of components exist"
    Params = MyParams

    def run(self, ctx):
        symbols = ctx.schematic.get_symbols()
        matching = [s for s in symbols if s.reference.startswith(self.params.prefix)]
        if len(matching) >= self.params.min_count:
            return CheckResult.pass_(self.id, f"Found {len(matching)} components")
        return CheckResult.fail(self.id, f"Only {len(matching)} components found")


result = MyComponentCountCheck(prefix="R", min_count=10)

TestStatus is one of PASS, FAIL, ERROR, or SKIP. Use SKIP for a check that cannot run in the current environment, such as a missing kicad-cli, so it is reported honestly rather than passing silently.

Development

uv sync --extra dev
uv run ruff check .
uv run mypy src
uv run pytest -m "not integration"

See CONTRIBUTING.md for the conventions, and CHANGELOG.md for what changed.

Documentation

  • Architecture — how parsing, geometry and the check registry fit together.
  • Contributing — setup, gates, and conventions.

License

MIT — see LICENSE.

Metadata

Release files for kicad-evaltor 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for kicad-evaltor 0.3.0
File Size Uploaded
kicad_evaltor-0.3.0.tar.gz 142.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for kicad-evaltor 0.3.0
File Interpreter ABI Platform
kicad_evaltor-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 238.6 kB

Release files / kicad_evaltor-0.3.0.tar.gz

Download URL kicad_evaltor-0.3.0.tar.gz
Size 142.1 kB
Tags Source
SHA-256 checksum
How to use checksums
a091247bd5e50bb16b2af8099c00f20e213794e6fa1fa9e1ccfa6aeb9c558ee5
BLAKE2b-256 checksum
How to use checksums
e9b0cef36c543db21c02b072e517783f3ae3d2d78417a3cc2108bf73e3761670
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

Release files / kicad_evaltor-0.3.0-py3-none-any.whl

Download URL kicad_evaltor-0.3.0-py3-none-any.whl
Size 96.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3397806bdf51817b8008e665e8e32c3f9de8f1aa3e518387379ac7722b85f6b2
BLAKE2b-256 checksum
How to use checksums
60ea8b3bb28e7d7e09e3042c880f66293606cb8190c36e9c1ad92ba908594475
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page