symdef
Standard-agnostic toolkit for graphical-symbol data. Symbols are TOML files; the toolkit loads them, resolves composition, lints the wiring contract, renders SVG, and builds a resolved JSON bundle plus a gallery. Data packages hold only data and reuse the toolkit and its gates, for any standard: IEC 60617, ISO 14617, ISA 5.1 or your own.
Two doors: symdef is what a consumer draws with (load, orient, render, measure), and
symdef.authoring is what a library's author builds with (build, check, init, lint,
write_build, stale_build, RULES, finding_key, LibraryConfig). No name is on both.
Status: alpha. The toolkit the spec below describes is built: reading, resolving, linting, SVG and
the build, with every lint rule and gate proven by a failing fixture. Published on PyPI from v0.4.0;
v0.1.0 to v0.3.0 were git tags named graphical-symbols.
Every example symbol in this repository, the docs and the gallery included, is drawn
independently in the style of a published standard (IEC 60617 for the S000xx numbers). None
is copied from the IEC database or from any standard's own drawings. Each is marked
status = "unverified": nobody has checked it against the standard.
Documentation: https://olejbondahl.github.io/symdef/ (tutorial, guide, symbol interface, API reference and a gallery).
Install
uv add symdef
or pip install symdef.
Python 3.14 or later. The one runtime dependency is fonttools, which measures a font's TrueType files.
The functions in symdef.files, symdef.project and symdef.cli read or write files, or
print. Everything else is pure: the same input gives the same output, with no file I/O, clock,
randomness or global state. The top-level symdef namespace re-exports both kinds. More
depth: docs/GUIDE.md.
The design
src/symdef/docs/SYMBOL_INTERFACE.html
is the binding, final spec. Open it in a browser; it ships as package data, readable at runtime
through importlib.resources.files("symdef").joinpath("docs/SYMBOL_INTERFACE.html").
The key ideas:
- A symbol is a definition: no tag, terminal numbers, position or orientation. Those belong to the placement in a drawing library.
- Port ids are roles (
in,out,anode, ...), never terminal markings (13,A1). - Electrical function is data: nodes and typed paths, so a consumer can netlist without per-symbol knowledge.
- Text lives in slots: the symbol reserves boxes for tags and terminal markings.
- Module units (M = 2.5 mm), a 0.125 M drawing grid and a 1 M wiring grid. A strict wiring contract is linted in all 8 orientations.
- Composition mirrors the standard: composites reference parts by anchor, no copied coordinates.
What is here
The data model and orient, the boxes and repeat, the TOML reader with its JSON Schema
(src/symdef/schema/symbol.schema.json), the composition resolver, 35 lint rules, plain and annotated SVG,
and the build (resolved JSON per symbol, the bundle, SVGs, a gallery README). Decisions the spec
leaves open, and concerns about the spec, are in docs/DECISIONS.md.
Use
A data repo has library.toml and symbols/<number>.toml; root below is its directory.
from symdef import load_library
from symdef.authoring import lint
from symdef.lint import RULES
library = load_library(root) # raises LibraryError, listing every problem in the files
contact = library.get("S00227") # by number; iterating a library is sorted by number
assert len(RULES) == 35 # the spec's 33 rules plus lead-off-port (D40) and stretch-cut
findings = lint(contact) # all 35 rules, geometric ones in all 8 orientations
assert findings == () # the spec's make contact lints clean
for finding in findings: # each has .rule, .severity, .message, .location, .orientation
print(finding.rule, finding.location, finding.message)
Symbols are immutable; orient, translate and repeat return new ones. A repeated symbol is
an ordinary symbol with ports such as 1.in and 3.out.
from symdef import Orientation, keepout_box, orient, repeat, to_svg
breaker = orient(repeat(contact, 3), Orientation.R0)
print(keepout_box(breaker))
svg = to_svg(breaker, annotate=True, texts={"tag": "-Q1"})
write_build writes build/ and src/<package>/bundle.json under the repo; stale_build lists
the files that differ from what it would write, so a test can fail when a committed build is out
of date. load_bundle reads a bundle back without the sources. <package> is the standard's
normalised name (lowercase letters and digits, D8): the example's standard is IEC 60617, so its
package is iec60617.
from symdef import load_bundle
from symdef.authoring import stale_build, write_build
write_build(library, root)
assert stale_build(library, root) == ()
assert len(load_bundle(root / "src" / "iec60617" / "bundle.json")) == len(library)
Development
uv sync
just ci # format check, ruff, ty, vulture, purity gate, pytest with coverage
Coverage must stay at 100% (--cov-fail-under=100), so a partial run such as
uv run pytest tests/test_units.py fails on coverage alone: add --no-cov for one.
tests/test_readme.py runs the examples above and those in docs/GUIDE.md, so
they stay true. Agents: start at AGENTS.md.
License
MIT, see LICENSE.