Skip to main content

pigtail

House wiring as code. Describe a home's electrical system in YAML — the panel and its breakers, the cables and conduit, every box, splice, and device — and pigtail draws it wire by wire, traces which circuit every conductor is on, and checks it against the National Electrical Code (NEC 2023): conductor sizing, box fill, conduit fill and derating, shared neutrals, continuous loads, voltage drop, and more.

Named after the pigtail: the short wire from a splice to a device terminal, found in almost every box it draws.

Install

pip install pigtail

Python 3.13 or later. Drawings need Graphviz (brew install graphviz, apt install graphviz).

Quickstart

Describe the wiring: what connects to what.

# Two branch circuits from one panel:
#   C1  15 A lighting: EMT to a switch box, a switch loop on to a ceiling light
#   C2  20 A receptacles: NM-B cable daisy-chained through two receptacle boxes
# C2 is wired in 14 AWG cable, which the check flags: 14 AWG is limited to 15 A.

meta:
  project: Quickstart
  code_edition: NEC-2023

boxes:                            # volume from Table 314.16(A), or as marked
  - {id: SW, type: device, material: metal, shape: square, trade_size: "4 x 1-1/2"}
  - {id: LT, type: outlet, material: metal, shape: round/octagonal, trade_size: "4 x 1-1/2"}
  - {id: R1, type: device, material: nonmetallic, volume_cu_in: 18.0}
  - {id: R2, type: device, material: nonmetallic, volume_cu_in: 18.0}

panelboards:
  - id: MP
    main_breaker: {ampere_rating: 100}
    branch_circuits:              # the breakers
      - id: C1
        circuit_breaker: {line: L1, ampere_rating: 15, afci: true}
      - id: C2
        circuit_breaker: {line: L2, ampere_rating: 20, afci: true}

    splices:                      # the landings: each wire on a breaker or a busbar
      - join: [E1.hot, C1.LOAD]
      - join: [E1.neutral, MP.NEUTRAL]
      - join: [E1.egc, MP.EGC]    # EMT: the tubing is the ground
      - join: [N1.ungrounded, C2.LOAD]
      - join: [N1.neutral, MP.NEUTRAL]
      - join: [N1.egc, MP.EGC]

    cables:                       # the runs leaving the panel, as a tree
      - id: E1
        type: EMT
        trade_size: "1/2"
        size_awg: 14
        length_ft: 18
        to: SW
        pulled:                   # EMT: each wire, by colour
          - {name: hot, role: ungrounded, color: black}
          - {name: neutral, role: neutral, color: white}
        devices:
          - {id: S1, type: snap_switch}
        splices:                  # in box SW
          - join: [E1.hot, S1.LINE]
          - join: [S1.LOAD, E2.leg]
          - join: [E1.neutral, E2.neutral]
          - join: [E1.egc, E2.egc, S1.EGC]
        cables:
          - id: E2
            type: EMT
            trade_size: "1/2"
            size_awg: 14
            length_ft: 8
            to: LT
            pulled:
              - {name: leg, role: ungrounded, color: red}      # the switched hot
              - {name: neutral, role: neutral, color: white}
            devices:
              - {id: L1, type: luminaire, load_va: 60}
            splices:              # in box LT
              - join: [E2.leg, L1.LINE]
              - join: [E2.neutral, L1.NEUTRAL]
              - join: [E2.egc, L1.EGC]

      - id: N1
        type: NM-B
        size_awg: 14              # on a 20 A breaker: flagged
        conductors: 2
        length_ft: 30
        to: R1
        devices:
          - {id: RA, type: receptacle, nema: 5-20R}
        splices:                  # in box R1
          - join: [N1.ungrounded, RA.LINE, N2.ungrounded]
          - join: [N1.neutral, RA.NEUTRAL, N2.neutral]
          - join: [N1.egc, RA.EGC, N2.egc]
        cables:
          - id: N2
            type: NM-B
            size_awg: 14
            conductors: 2
            length_ft: 12
            to: R2
            devices:
              - {id: RB, type: receptacle, nema: 5-20R}
            splices:              # in box R2
              - join: [N2.ungrounded, RB.LINE]
              - join: [N2.neutral, RB.NEUTRAL]
              - join: [N2.egc, RB.EGC]

Draw it:

pigtail draw examples/quickstart.yaml -o quickstart.svg

The quickstart wiring, drawn by pigtail: the panel with breakers C1 and C2, the EMT runs E1 and E2 through switch box SW to light box LT, and the NM-B runs N1 and N2 through receptacle boxes R1 and R2

Check it:

$ pigtail check examples/quickstart.yaml
240.4(D): cable N1 -- N1.ungrounded (14 AWG NM-B 14/2) carries 15 A, under circuit C2's 20 A breaker
240.4(D): cable N2 -- N2.ungrounded (14 AWG NM-B 14/2) carries 15 A, under circuit C2's 20 A breaker
Table 250.122: cable N1 -- EGC N1.egc is 14 AWG; a 20 A circuit needs 12 AWG
Table 250.122: cable N2 -- EGC N2.egc is 14 AWG; a 20 A circuit needs 12 AWG

4 violation(s)

Features

  • Topology in, circuits out. The YAML says only what connects to what. pigtail traces each hot back to its breaker (through splices and switches) and each neutral out to the loads it returns — so shared neutrals (multiwire branch circuits) are found, not declared.
  • Real wiring methods. NM-B cable; EMT conduit with each wire pulled in by colour and tape, the tubing as the equipment ground; knob-and-tube, as found in older houses.
  • A conductor-level drawing. Every wire in its own colour, every box with its wire nuts, box volume and box fill on each box; SVG, PNG, or PDF. Every element carries a circuit-<id> SVG class, so a viewer can hide or show circuits.
  • NEC checks, warn-only. An as-built house can hold violations; pigtail reports them instead of refusing the model. Each finding cites its NEC section.
  • Honest about missing data. A rule that needs a value you haven't recorded (a box volume, a run length, a load) is reported as not checked, never guessed.
  • Box fill (314.16) from a box's Table 314.16(A) trade size or its marked volume, plus extension rings, plaster rings, and covers.
  • Conduit fill and ampacity (Chapter 9, 310.15, 110.14(C)): fill against Table 1, current-carrying conductors and their adjustment, ambient correction, terminal temperature limits, 240.4(B) and 240.4(D).
  • Loads: continuous loads and EV charging at 125 % (210.19, 210.20, 625.41), receptacle ratings (210.21(B)), voltage drop (210.19(A) Informational Note No. 4).
  • Bill of materials: cable and conduit footage, THHN per gauge, boxes (with the ones too small and a standard size that fits), devices, breakers, connectors.
  • Every NEC number in one table module (pigtail.nec_tables), named after its table: TABLE_310_16, TABLE_314_16_A, CHAPTER_9_TABLE_5, ...
  • KiCad netlist and schedules: panel, conductor, raceway, and net schedules; the circuit is a SKiDL netlist underneath.

Command line

pigtail check house.yaml                  # NEC findings, then the rules not checked
pigtail draw  house.yaml -o house.svg     # the drawing (.svg, .png, .pdf); --circuit C1 for one
pigtail bom   house.yaml                  # bill of materials
pigtail build house.yaml --out ./out      # netlist, DOT, panel / conductor / raceway schedules

Python

import warnings
from pathlib import Path

from pigtail.models.house import House
from pigtail.nec import NECViolation
from pigtail.outputs.diagram import circuit_diagram

house = House.from_yaml(Path("house.yaml"))  # House.from_dict(...) also works
dot = circuit_diagram(house, only="C1")  # Graphviz DOT source

with warnings.catch_warnings(record=True) as found:
    warnings.simplefilter("always")
    house.check()  # one NECViolation / NECUnchecked warning per finding
for w in found:
    kind = "violation" if issubclass(w.category, NECViolation) else "not checked"
    print(kind, w.message)

bc = house.branch_circuit("C2")
print([c.id for c in bc.all_cables()], [d.id for d in bc.devices()])  # traced
print(house.raceway_schedule())
print(house.bom())

The YAML

  • boxes — every box, top-level (one box can hold several circuits): id, type (device, outlet, junction, pull), material (metal, nonmetallic), and its volume one way: shape + trade_size from Table 314.16(A) (square / "4 x 2-1/8", round/octagonal, device, ...), or volume_cu_in as marked. additions: extension rings, plaster rings, domed or raised covers, each by trade size or marked volume.
  • panelboards — id, main_breaker, service; branch_circuits (the breakers: ampere_rating, poles, line L1/L2 for 1-pole, afci, gfci, terminal_c); splices, the landings: each wire on <circuit>.LOAD (2-pole: LOAD1 / LOAD2), <panel>.NEUTRAL, or <panel>.EGC; cables, the runs leaving the panel.
  • cables — one run each, as a tree: the box it lands in (to; left out when its far end isn't traced yet), then that box's devices, splices, and onward cables.
    • type: NM-B | EMT | knob_and_tube, size_awg, length_ft, EMT trade_size, ambient_c, bends_deg.
    • Cable assemblies give conductors: 2 | 3 (the "/2", "/3"; the ground wire is added); reidentify marks a re-taped white (200.7(C)(1)).
    • EMT runs list every wire pulled: name, role (ungrounded, neutral, traveler, egc), color, tape, and optionally size_awg, insulation, stranding. The tubing is the run's ground, <run>.egc.
  • devices — snap_switch (LINE, LOAD, EGC); luminaire (load_va); receptacle (nema, poles, gfci, terminal_c, load_va, continuous); appliance, hardwired (load_va, poles). Terminals: LINE, NEUTRAL, EGC, or LINE1 / LINE2 at 240 V.
  • splices — wire nuts: join: [<run>.<wire>, <device>.<TERMINAL>, ...]. One member is a wire capped alone; through: true is one wire passing through a box unspliced.

Leave a value out rather than guess it: pigtail reports what it then can't check.

NEC checks

Section Check
110.7, 200.11 the wiring agrees with each conductor's declared role
110.14(C), 310.15, 240.4 ampacity in conduit: insulation, adjustment, ambient, terminals
200.4(A), 210.4(B) shared neutrals: opposite legs, simultaneous disconnect, sizing
200.6(A), 200.7(C)(1) neutral colour; re-identified whites
210.12(A) AFCI on 120 V 15/20 A circuits
210.19(A), 210.20(A), 625.41 continuous loads at 125 %
210.19(A) IN No. 4 voltage drop over 3 %
210.21(B) receptacle rating against its circuit
240.4(D), 334.80 small conductors and NM cable
Table 250.122 equipment grounding conductor size
310.3(C) 8 AWG and larger stranded in conduit
314.16 box volume against box fill
358.22, Chapter 9 conduit fill
358.26 bends between pull points
394.12 knob-and-tube
406.4(D)(2), 406.12 ungrounded and tamper-resistant receptacles

Rules that depend on data the model doesn't carry — a room's use (e.g. where GFCI is required), mounting heights, geometry — are left out rather than guessed.

Limits

pigtail is a modelling and checking aid. It is not a substitute for the Code itself, for your authority having jurisdiction (which may amend the NEC), or for a licensed electrician. Findings cover only what the model records.

NEC values are transcribed from NFPA 70, National Electrical Code, 2023 edition. pigtail is not affiliated with or endorsed by the NFPA. NEC® and National Electrical Code® are registered trademarks of the National Fire Protection Association.

Development

uv sync
uv run ruff check && uv run ruff format --check && uv run mypy && uv run pytest

See AGENTS.md for the layout and design rules.

License

MIT

Release files for pigtail 0.1.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 pigtail 0.1.0
File Size Uploaded
pigtail-0.1.0.tar.gz 49.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pigtail 0.1.0
File Interpreter ABI Platform
pigtail-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 110.8 kB

Release files / pigtail-0.1.0.tar.gz

Download URL pigtail-0.1.0.tar.gz
Size 49.6 kB
Tags Source
SHA-256 checksum
How to use checksums
dc481476aad4d75c2ea4d842feef2e130e3a95fd3826467c45d3c30c7e9c04c9
BLAKE2b-256 checksum
How to use checksums
8ef8cd16bfca34435323f3647500ea2f241d5331b93f977ecbd5842902e5b22e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","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}

Release files / pigtail-0.1.0-py3-none-any.whl

Download URL pigtail-0.1.0-py3-none-any.whl
Size 61.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
27c5dde9e7c3c91a6d4b362f5f21fb16a265be86042be567cd665f87529a3774
BLAKE2b-256 checksum
How to use checksums
8e138367260475314bbec93f7eab0b841cb680792ab0495fe3a67e485c8e303b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","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}

Release history Release notifications | RSS feed

This release

0.1.0 This release

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