Verilog meets Python: describing hardware, the scripted way
Verilog is how you tell silicon what to become. Python is how you tell Verilog what to do — generate it, drive it, and check that it behaves. Here's how the two actually fit together, with real code.
.v
simulator in the middle
.py
01 Where Python actually fits into a Verilog flow
Python never replaces Verilog on silicon. It sits around it.
Verilog is a hardware description language: every line describes wires, registers, and logic that a synthesis tool eventually turns into real gates on a chip or an FPGA. Python is a general-purpose scripting language with no concept of clock edges or bit widths unless you teach it some. So "Verilog with Python" almost always means one of three things:
- Verification — Python drives a Verilog design under simulation and checks its outputs (this is what
cocotbdoes). - Generation — you write the hardware description in Python and a library emits Verilog for you to synthesize (this is what
MyHDLandAmaranthdo). - Automation — Python glues the flow together: running simulators, parsing logs, scripting synthesis, generating register maps or documentation from a spec.
The rest of this post walks through the first two in detail, since they're where most of the interesting code lives.
02 Verifying Verilog with Python — cocotb
Write the design in Verilog. Write the testbench in Python.
cocotb is a coroutine-based verification library that lets you write testbenches in plain Python using async/await, while an underlying simulator (Icarus Verilog, Verilator, Questa, and others) runs your actual Verilog RTL. Python reads and drives signals on the design under test (DUT) exactly like a real testbench would, just without Verilog's clunkier procedural syntax.
The design under test
module counter #(parameter WIDTH = 8) (
input wire clk,
input wire rst,
input wire enable,
output reg [WIDTH-1:0] count
);
always @(posedge clk or posedge rst) begin
if (rst)
count <= 0;
else if (enable)
count <= count + 1'b1;
end
endmodule
The Python testbench
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge
@cocotb.test()
async def counter_counts_when_enabled(dut):
# Start a 100 MHz clock on dut.clk
cocotb.start_soon(Clock(dut.clk, 10, units="ns").start())
# Reset the design
dut.rst.value = 1
dut.enable.value = 0
await RisingEdge(dut.clk)
dut.rst.value = 0
await RisingEdge(dut.clk)
# Enable and check five consecutive counts
dut.enable.value = 1
for expected in range(1, 6):
await RisingEdge(dut.clk)
actual = int(dut.count.value)
assert actual == expected, f"expected {expected}, got {actual}"
dut._log.info("Counter behaved correctly for 5 cycles")
Run it with a small Makefile that points cocotb at the simulator and the source file, and you get a normal Python test report — pass/fail, tracebacks, assertion messages — instead of parsing raw $display output from a Verilog testbench.
numpy, coverage tools, CI integration) around a DUT that's still pure, synthesizable Verilog.
03 Generating Verilog from Python — MyHDL & Amaranth
Write the hardware description itself in Python, then convert it.
A different family of tools lets you skip writing Verilog by hand altogether. You describe registers and combinational logic in Python using a small, disciplined subset of the language, and the library elaborates that description into synthesizable Verilog (or VHDL). Two well-known options:
- MyHDL — one of the oldest Python-to-HDL projects. You write generator functions decorated as hardware blocks, simulate them directly in Python, then call
.convert()to emit Verilog. - Amaranth (formerly nMigen) — a newer, more actively maintained toolbox with its own statement/expression model, built-in simulator, and Verilog backend via Yosys.
The same counter, described in MyHDL
from myhdl import block, always_seq, Signal, intbv, ResetSignal
@block
def counter(clk, rst, enable, count):
@always_seq(clk.posedge, reset=rst)
def logic():
if enable:
count.next = count + 1
return logic
# Elaborate signals, then generate synthesizable Verilog
clk = Signal(bool(0))
rst = ResetSignal(0, active=1, isasync=False)
enable = Signal(bool(0))
count = Signal(intbv(0)[8:])
inst = counter(clk, rst, enable, count)
inst.convert(hdl='Verilog') # writes counter.v
The generated counter.v is ordinary, readable RTL — the same kind of always-block you'd write by hand — but the source of truth lives in Python, where you get real functions, loops, classes, and parametrization instead of Verilog's generate blocks and macros.
generate statements.
04 Verilog vs. Python-for-hardware, side by side
| Aspect | Verilog | Python (MyHDL / Amaranth / cocotb) |
|---|---|---|
| Primary role | Hardware description & synthesis input | Generation, verification, and automation around it |
| Runs on silicon? | Yes — synthesized directly | Only indirectly, by emitting Verilog first |
| Testbenches | Possible, but verbose and weakly typed | Rich assertions, logging, randomization via cocotb |
| Parametrized designs | generate blocks, macros | Ordinary functions, loops, classes |
| Tool maturity | Decades old, universally supported by every vendor | Smaller ecosystem, dependent on Verilog/VHDL backends |
| Learning curve | New concurrency & timing model | Familiar syntax, but hardware semantics still to learn |
| Best for | Production RTL, timing-critical logic | Fast iteration, verification, repetitive structures |
05 Which one should you reach for?
In practice, most real projects use both, split cleanly by job:
- Design in Verilog when you need every synthesis and timing tool in the industry to understand your code without translation — which is still most production silicon and FPGA work.
- Verify with cocotb whenever the testbench logic — stimulus generation, scoreboarding, coverage — is more complex than a handful of directed test vectors.
- Generate with MyHDL or Amaranth when the design itself is highly parametric, repetitive, or benefits from being described as a program rather than a static netlist-in-waiting.
None of this replaces learning Verilog's own concurrency model — blocking vs. non-blocking assignment, sensitivity lists, race conditions between always blocks. Python only removes the pain around the design, not the design itself.

0 Comments