CAD as Code with build123d: Change One Number, Rebuild a 220-Hole Facade Cassette
Learn CAD as code with build123d 0.13: model a 1200 x 600 mm aluminium cassette with 220 graded holes in Python, print mass and open area, export STEP.
A Rhino user draws a solid, but a build123d user writes the program that produces it. That is the essence of code-CAD. Every dimension of the part is a named number at the top of a file, and when you change one number and press run, the whole solid is rebuilt: holes, fillets, mass, export. In this tutorial you write one screen of Python that models a 1200 x 600 x 3 mm aluminium facade cassette. It has 220 perforations that grow row by row, from 8 mm at the bottom to 30 mm at the top, plus four fixing holes. The script prints the mass and the open area, then writes a STEP file that opens in Rhino 8.
The light hook: build123d 0.13.0 shipped on 21 September 2026, and code-CAD kept reaching the Hacker News front page this year. Hackaday, covering the Rust-written SolidCraft this month, gave the clearest one-line definition of the idea: in a parametric timeline, the model exists “as the sum of a sequence of operations.” build123d takes that sentence literally. The sequence is your Python file.
The Tool: build123d, by Roger Maitland (GitHub: gumyr) and contributors, released under Apache 2.0. Its README describes it as “a Python-based, parametric (BREP) modeling framework for 2D and 3D CAD”, built on the Open Cascade geometric kernel. A kernel is the engine underneath any CAD program that computes intersections, booleans and exact surfaces. Maitland’s README credits its lineage plainly: build123d “is derived from portions of CadQuery”, and it was refactored into an independent framework with its own builder syntax. Taking a kernel that only a few specialists could drive and turning it into a library an architect can read on a Sunday is engineering work, and it is the harder half. Rhino and Grasshopper stay the house tools for design exploration. build123d is the complement for precise, repeatable parts that are handed to fabrication: one script, a hundred variants, exact BREP solids. BREP means boundary representation, a solid described by its exact bounding faces rather than by a mesh of triangles.
Setup: build123d 0.13 needs Python 3.11 or newer (and below 3.15). Rhino 8 ships its own CPython 3.9 for ScriptEditor, so build123d lives in its own Python environment next to Rhino, not inside it. The hand-off between the two is the STEP file. That is a clean division of labour: the script makes the part, and Rhino places it in the building.
# Windows (macOS/Linux: source cad-env/bin/activate)
python --version # must be 3.11 or newer
python -m venv cad-env
cad-env\Scripts\activate
pip install build123d
python -c "import build123d; print(build123d.__version__)" # 0.13.0
On import, build123d may print one font warning line (“name table stringOffset incorrect”). It is harmless, so ignore it. A venv, or virtual environment, is a private folder of Python packages. It keeps this project’s libraries away from everything else on the machine.
First steps:
- Create
first.pyin algebra mode, where solids combine with + and – like numbers:from build123d import * plate = Box(200, 100, 10) - Cylinder(radius=15, height=10) print(plate.volume) export_step(plate, "first.step") - Run
python first.py. It prints192931.41652942295. - Check it by hand: 200 x 100 x 10 = 200,000 mm³, minus π x 15² x 10 = 7,068.6 mm³, leaves 192,931.4 mm³. That matches to the last decimal. Box and Cylinder are both centred on the origin, so the hole lands in the middle. Units are millimetres by default.
- In Rhino 8, use File > Import and pick
first.step. Your first program is now a solid on the canvas.
The code, explained: here is panel.py in builder mode, where with blocks open workspaces and shapes made inside them join the part automatically.
# panel.py - a perforated facade panel, every number a parameter
from build123d import *
W, H, T = 1200, 600, 3 # panel width, height, thickness (mm)
CORNER = 20 # corner radius (mm)
PITCH = 50 # hole spacing (mm)
D_MIN, D_MAX = 8, 30 # smallest and largest hole (mm)
ROWS, COLS = 10, 22 # perforation grid
with BuildPart() as panel:
with BuildSketch():
RectangleRounded(W, H, CORNER)
extrude(amount=T)
top = panel.faces().sort_by(Axis.Z)[-1] # the face we drill into
with BuildSketch(top):
for r in range(ROWS):
d = D_MIN + (D_MAX - D_MIN) * r / (ROWS - 1) # row 0 small, top row big
y = (r - (ROWS - 1) / 2) * PITCH
with Locations((0, y)):
with GridLocations(PITCH, PITCH, COLS, 1):
Circle(d / 2)
with GridLocations(W - 60, H - 60, 2, 2): # four fixing holes
Circle(11 / 2)
extrude(amount=-T, mode=Mode.SUBTRACT)
part = panel.part
open_area = 1 - part.volume / (W * H * T)
print(f"volume {part.volume / 1000:.0f} cm3, mass {part.volume * 2.7e-6:.2f} kg (aluminium)")
print(f"open area {open_area:.1%}, faces {len(part.faces())}")
export_step(part, "panel.step")
export_stl(part, "panel.stl")
Run python panel.py. It takes about nine seconds and prints:
volume 1945 cm3, mass 5.25 kg (aluminium)
open area 9.9%, faces 234
Lines 4–8 — the constants are the design: a Grasshopper user knows these as sliders. Here they are named numbers in one place, each with its unit in the comment. A parameter is just a value that the rest of the program reads instead of repeating. Change PITCH and every hole moves, because no other line hard-codes 50.
Lines 10–13 — the plate: with BuildPart() as panel: opens a workspace for a 3D part. Inside it, BuildSketch() opens a flat drawing on the XY plane. RectangleRounded(W, H, CORNER) draws the outline with 20 mm corners, and extrude(amount=T) pushes it up into a 3 mm plate. Indentation is the grammar here: what sits inside a with block belongs to that workspace.
Line 15 — selecting by asking: panel.faces().sort_by(Axis.Z)[-1] gets every face, sorts them by height, and takes the last one, which is the highest. You asked a question, and the answer stays correct even if T changes. Selection by query is the core idea of code-CAD.
Lines 16–24 — the graded loop: a loop repeats a block once per value, here once per row r from 0 to 9. r / (ROWS - 1) runs from 0 to 1, so the diameter d runs linearly from 8 to 30 mm. Locations((0, y)) shifts each row vertically, centred on the panel. GridLocations(PITCH, PITCH, COLS, 1) repeats the circle 22 times across. In the exported file, radii run from 4.0 mm in the row at y = -225 up to 15.0 mm at y = +225, so the large holes sit at the top, where more daylight and view are wanted. The second GridLocations places four 11 mm fixing holes 30 mm in from each edge.
Line 25 — one cut, 224 holes: extrude(amount=-T, mode=Mode.SUBTRACT) pushes every circle downward through the plate and removes it in a single boolean. The beginner mistake is to forget mode=Mode.SUBTRACT. The circles are then added as 3 mm discs on top of the plate. You spot it straight away because the volume goes up instead of down.
Lines 27–32 — the printed checks and the export: volume is in mm³. 2.7e-6 kg/mm³ is aluminium’s density, 2.7 g/cm³. Open area is one minus the solid volume divided by the full plate volume of 2,160,000 mm³. A hand calculation of the hole areas also gives 9.9 %. Face count: top and bottom, 8 edge faces (4 sides plus 4 rounded corners), and 224 hole walls make 234. The STEP file is about 0.9 MB and the STL about 5.7 MB. It re-imports as one valid solid with a 1200.0 x 600.0 x 3.0 bounding box.
A benchmark on the ModelRift blog, discussed on Hacker News on 12 September, had AI agents model printable parts in CadQuery and OpenSCAD. Every part ended up printable, but the runs hit 16 failures, “9 of which the tool never mentioned at all.” Whoever writes the CAD code, human or model, check the solid: its volume, its validity, one hand calculation.
←TODAY: a 1200 x 600 mm cassette is 32 lines of Python and a 0.9 MB STEP file in October 2026.
→3012: the panel outlives its software, because its recipe is plain text and its solid is an open ISO standard.
Fulcrum: a part you can re-run is a part you can repair.
From the late 2070s, the facades that aged worst were the ones whose panel geometry lived only inside a dead proprietary file, so a damaged cassette could not be remade. A text script plus a STEP file (ISO 10303) is the opposite: a 25-year-old can open both in a plain editor and a free kernel. Choose formats with that reader in mind.
Atelier: a Swiss facade-planning office preparing a perforated cassette family for a metalworker designs and explores in Rhino and Grasshopper, then hands the agreed parameters to a script that writes one exact STEP per panel type for the fabricator’s CAM. The model stops being the drawing and becomes a recipe the office can version, diff and re-run when the grid changes the night before the tender. Monday move: pick the one panel type your office draws most often, write its dimensions as the first five lines of a build123d script, and keep that script in the same project folder as the Rhino file.
Hack: Open the perforation wider by editing a single number, and predict the result before you run it. Larger top holes should mean more open area and less mass, while the face count stays the same because there are still 224 holes. Change line 7, run, and compare:
# panel.py, line 7 — the only edit
D_MIN, D_MAX = 8, 40 # was 8, 30
# python panel.py -> volume 1805 cm3, mass 4.87 kg (aluminium)
# open area 16.4%, faces 234
Open area rises from 9.9 % to 16.4 % and the cassette loses 0.38 kg. The tightest web left between neighbouring top-row holes is now 10 mm (a 50 mm pitch minus a 40 mm hole). Whether 10 mm is enough is a question for the facade engineer, not for the script. That is the trade-off in plain terms: the script makes the variant instantly, but it cannot sign it off.
Quiet credit: behind every boolean here sits the Open Cascade kernel, decades of geometry code maintained by engineers almost nobody names, and Maitland’s work in making it speak a builder’s language. At PAZ, a Rhino centre in Switzerland, we teach Rhino 8 and Grasshopper; code-CAD is the next drawer in the same toolbox. Two languages, one building, zero dialog boxes.
Learn-it:
- The repo: github.com/gumyr/build123d
- Docs, Introductory Examples: build123d.readthedocs.io — introductory examples
- Cheat Sheet, every builder and operation on one page: build123d cheat sheet
- See the solid while you type, using the OCP CAD Viewer for VS Code (
pip install ocp-vscode, thenshow(part)): vscode-ocp-cad-viewer - The root concept: Boundary representation (Wikipedia)
- PAZ note: rename
panel.pyafter the panel type it makes, and the file name becomes your parts catalogue.
Clone the repo, run first.py, check the volume by hand, and then change one number in panel.py before lunch.
PAZ Kaffi · multidisciplinary editorial, led by PAZ Academy