Write ladder logic in Python. Simulate it. Test it. Deploy it.
pyrung turns Python's with block into a ladder rung — condition on the rail, instructions in the body.
from pyrung import Bool, PLCRunner, Program, Rung, out
Button = Bool("Button")
Light = Bool("Light")
with Program() as logic:
with Rung(Button):
out(Light)
runner = PLCRunner(logic)
with runner.active():
Button.value = True
runner.step()
assert Light.value is TrueStatus: Core engine, Click PLC dialect, CircuitPython dialect, and VS Code debugger are implemented and tested (~26k lines, 1,600+ tests). Not yet on PyPI. API may still change.
- Documentation: https://ssweber.github.io/pyrung/
- LLM docs index: https://ssweber.github.io/pyrung/llms.txt
AutomationDirect CLICK PLCs have no built-in simulator. You write logic, download it to hardware, and hope. pyrung lets you test first — same tag names, deterministic scans, real assertions. When it works, transpose it to Click.
Or don't transpose at all. Run your program as a soft PLC to test Modbus send/receive — it runs behind a Click-compatible Modbus interface, no hardware required. You can even spin up two pyrung programs and test them talking to each other. Or generate a CircuitPython scan loop for a ProductivityOpen P1AM-200 and run it on actual I/O.
# Requires Python 3.11+
uv add pyrungfrom pyrung import Bool, Program, Rung, latch, reset
Start = Bool("Start")
Stop = Bool("Stop")
Running = Bool("Running")
with Program() as logic:
with Rung(Start):
latch(Running)
with Rung(Stop):
reset(Running)from pyrung import PLCRunner
runner = PLCRunner(logic)
with runner.active():
Start.value = True
runner.step()
assert Running.value is True
# Release start — motor stays latched
Start.value = False
runner.step()
assert Running.value is True
Stop.value = True
runner.step()
assert Running.value is Falsefrom pyrung.click import TagMap
tags = TagMap()
tags.map(Start, "X001") # Physical input
tags.map(Stop, "X002") # Physical input
tags.map(Running, "Y001") # Physical output
tags.validate(logic) # Checks against Click constraints
tags.export_nicknames("motor.csv") # For Click programming softwareCore engine — Pure f(state) → new_state scan cycle with immutable snapshots. Coils, latches, timers, counters, branching, subroutines, structured tags, edge detection, and more. Built to match real Click behavior — no surprises when you move to hardware.
Click PLC dialect — Hardware address mapping, memory bank validation, Modbus instructions, and nickname file export. Run any program as a soft PLC behind a Click-compatible Modbus interface for integration testing. Docs →
CircuitPython dialect — Generates a self-contained scan loop for P1AM-200 hardware from any pyrung program. Docs →
VS Code debugger — Step through scans, set breakpoints on rungs, force tags, diff states, and time-travel through scan history. Docs →
| Core Concepts | Scan cycle, SystemState, tags, blocks |
| Ladder Logic Guide | Full DSL reference |
| Tag Structures | UDTs, named arrays, cloning, block config |
| Runner Guide | Execution, time modes, history, fork |
| Testing Guide | Unit testing with deterministic time |
| Forces & Debug | Force values, breakpoints, time travel |