OpenSCAD language reference: syntax, variables, for loops and if
OpenSCAD is a declarative language. Every variable has one value per scope, for loops
make copies of geometry instead of updating counters, and functions return values while modules
produce shapes. Once those three rules click, the rest of the syntax reads like any C-style language.
Every example below was run through OpenSCAD, and echo-only snippets show their real console output.
- Statements end with
;. A transform, boolean orifapplies to the next statement or{ }block. - Seven types: number, boolean, string, list, range,
undefand function. - Variables can't be changed.
formakes geometry, and list comprehensions make lists. ifchooses geometry, andcond ? a : bchooses values.- Load BOSL2 with
include. Useusefor files that only define modules and functions.
OpenSCAD syntax: statements, blocks and comments
Whitespace doesn't matter. Each statement ends with a semicolon. Transforms, booleans, for and
if act on the one statement after them, or on a { } block of statements.
A module definition ends at its closing brace, and a function definition ends with a semicolon.
// line comment
/* block
comment */
size = 20; // assignment
cube(size); // a statement that makes geometry
translate([30, 0, 0]) { // a block: the move applies to everything inside
cube(10);
sphere(5);
}
OpenSCAD data types
| Type | Examples | Test |
|---|---|---|
| Number | 3, -2.5, 1e6, PI | is_num() |
| Boolean | true, false | is_bool() |
| String | "M3", "line\n" | is_string() |
| List (vector) | [1, 2, 3], ["a", 2, [true]] | is_list() |
| Range | [0 : 5], [0 : 0.5 : 10] | None (is_list() is false) |
| Undefined | undef | is_undef() |
| Function (2021.01+) | function (x) x * 2 | is_function() |
false, 0, "", [] and undef. Everything else is true, including the string "0"."a" + "b", undef + 1 and "5" < 6 all give undef with a warning.OpenSCAD variables and scope: why you can't reassign
OpenSCAD works out all the variables in a scope before it builds any geometry. A name holds one value, and if you assign it twice, the last assignment wins everywhere in that scope, even on earlier lines:
x = 1;
echo(x); // ECHO: 2
x = 2; // recent versions also warn that x was overwritten
Every module body, function call, for body, if branch and let opens a new scope.
An assignment inside it creates a new local variable that hides the outer one without changing it. That's why
the classic counter doesn't count:
total = 0;
for (v = [1, 2, 3]) {
total = total + v; // a new local total on every pass: 0 + v
echo(total); // ECHO: 1, then 2, then 3
}
echo(total); // ECHO: 0
To accumulate, describe the result instead of the steps. A recursive function does it in one line:
function sum(v, i = 0) = i < len(v) ? v[i] + sum(v, i + 1) : 0;
echo(sum([1, 2, 3])); // ECHO: 6
Top-level variables are also your model's parameters. Desktop OpenSCAD's Customizer and PrintPal's Params tab show them as
sliders, and openscad -D leg=60 -o out.stl part.scad overrides one from the command line.
The parametric design guide covers how to lay them out.
What does this print? w = 10; echo(w * 2); w = 25;
The last assignment wins for the whole scope, so w is 25 everywhere and the echo prints 50. Recent versions also print a warning that w was overwritten.
Vectors and lists: indexing, dot notation and vector maths
A list holds any mix of values, including other lists. Points, sizes and matrices are all just lists:
p = [10, 20, 30];
echo(p[0]); // 10
echo(p.y); // 20 (only .x, .y and .z exist)
echo(len(p)); // 3
echo(p[5]); // undef, no error
echo(concat(p, [40])); // [10, 20, 30, 40]
echo(p + [1, 1, 1]); // [11, 21, 31], element by element
echo(p * 2); // [20, 40, 60]
echo([1, 0, 0] * p); // 10, the dot product
m = [[0, -1, 0], [1, 0, 0], [0, 0, 1]]; // 90° turn about Z
echo(m * [10, 0, 0]); // [0, 10, 0]
Ranges: [start : step : end]
A range is a lazy sequence of numbers, mostly used to drive loops. The step is optional and defaults to 1. The end value is included only if a step lands on it:
for (i = [0 : 5 : 20]) echo(i); // 0, 5, 10, 15, 20
for (i = [0 : 4]) echo(i); // 0, 1, 2, 3, 4
for (i = [10 : -2 : 0]) echo(i); // 10, 8, 6, 4, 2, 0
for (i = [0 : 3 : 10]) echo(i); // 0, 3, 6, 9
[5 : 0].
OpenSCAD 2021.01 swaps the ends of [5 : 0] and prints a deprecation notice. Current snapshots warn and
produce nothing at all. [5 : -1 : 0] works everywhere. A range isn't a list either: to get one, write [for (i = [0 : 3]) i].
OpenSCAD if / else and the ternary operator
thick = 6;
if (thick > 5) {
cube([20, 20, thick]);
} else if (thick > 2) {
cube([20, 20, 5]);
} else {
echo("Too thin to print");
}
To choose a value, use the ternary operator: h = thick > 5 ? 10 : 5;
if braces stay inside.
The braces are a scope, so if (rounded) { r = 3; } followed by echo(r); gives
WARNING: Ignoring unknown variable "r" and undef. Without braces, if (rounded) r = 3;
is a syntax error. Write r = rounded ? 3 : 0; instead.
OpenSCAD for loops: rows, grids and intersection_for
A for loop makes one copy of its body per value, and the copies are combined into one object.
// a row of 5 pegs, 10 mm apart
for (x = [0 : 10 : 40])
translate([x, 0, 0]) cylinder(h = 8, d = 4, $fn = 24);
// a 3 × 2 grid: two loop variables give every combination
for (x = [0 : 2], y = [0 : 1])
translate([x * 12, 20 + y * 12, 0]) cube(8);
You can loop over any list too: for (p = [[0, 0], [15, 5]]), or over the characters of a string (2019.05+).
intersection_for keeps only the volume that every copy shares:
// three slabs at 0°, 60° and 120° leave a hexagonal prism, 20 mm across flats
intersection_for (a = [0, 60, 120])
rotate([0, 0, a]) cube([40, 20, 10], center = true);
[for (i = 0; i < 4; i = i + 1) i] is valid (2019.05+), but a geometry for statement needs a range or a list.
List comprehensions: build points, sizes and filters
A list comprehension is a for inside square brackets that returns a list. It's the standard way to compute points for polygon() or polyhedron().
evens = [for (i = [0 : 10]) if (i % 2 == 0) i]; // [0, 2, 4, 6, 8, 10]
signs = [for (i = [0 : 3]) if (i % 2 == 0) i else -i]; // [0, -1, 2, -3]
flat = [for (row = [[1, 2], [3]]) each row]; // [1, 2, 3]
echo(evens, signs, flat);
// a 12-pointed star: 24 points alternating between r = 20 and r = 12
pts = [for (i = [0 : 23]) let (r = i % 2 == 0 ? 20 : 12) [r * cos(i * 15), r * sin(i * 15)]];
linear_extrude(height = 3) polygon(pts);
What does [for (i = [0 : 2 : 6]) i] produce?
The range starts at 0, steps by 2, and includes the end because 6 is reached exactly. The comprehension turns it into a real list.
let(): local names inside expressions and modules
area = let (w = 20, h = 30) w * h; // 600
b = let (a = 2, b = a * 3) b; // 6: later names can use earlier ones
ring = [for (a = [0 : 30 : 330]) let (r = 25) [r * cos(a), r * sin(a)]];
echo(area, b, len(ring)); // 600, 6, 12
Since 2019.05 let also works as a module, so a few names apply to just one piece of geometry:
let (r = 10, h = 20) cylinder(h = h, r = r, $fn = 48);
Functions and modules: values versus geometry
| Function | Module | |
|---|---|---|
| Definition | function name(a, b = 1) = expr; | module name(a, b = 1) { … } |
| Produces | A value | Geometry |
| Called | Inside an expression | As a statement |
| Takes child shapes | No | Yes, via children() |
| Can recurse | Yes (deep tail recursion is safe since 2021.01) | Yes |
function clearance(d) = d + 0.4; // returns a value
module peg(d = 5, h = 10) { // makes geometry
cylinder(h = h, d = d, $fn = 32);
}
module ring_of(n = 6, r = 20) { // repeats whatever you pass in
for (i = [0 : n - 1])
rotate([0, 0, i * 360 / n]) translate([r, 0, 0]) children();
}
ring_of(8) peg(d = clearance(4));
Since 2021.01 functions are values too, so you can store and pass them:
double = function (x) x * 2;
echo(double(21)); // ECHO: 42
Default arguments, named calls and the children() patterns are covered in modules and functions.
Special $ variables and dynamic scope
Variables that start with $ are passed down into every module and function called from where they're set.
Set $fn on one module call and every curve inside it changes:
$fa = 1;
$fs = 0.4; // file-wide default: smooth curves sized to fit
module knob() {
cylinder(h = 10, d = 20);
translate([0, 0, 10]) sphere(d = 12);
}
knob();
translate([30, 0, 0]) knob($fn = 8); // only this copy: chunky octagons
| Variable | Default | Purpose |
|---|---|---|
$fn | 0 | Exact segment count when above 0. Overrides $fa/$fs |
$fa / $fs | 12° / 2 mm | Minimum angle and minimum length per segment |
$t | 0 | Animation time, from 0 up to but never reaching 1 |
$preview | n/a | true in F5 preview, false in an F6 render or export (2019.05+) |
$children | n/a | Number of children passed to the current module |
$vpr $vpt $vpd $vpf | n/a | Viewport rotation, translation, distance and field of view ($vpf 2021.01+) |
$fa/$fs for the file and $fn for exceptions.
$fa = 1; $fs = 0.4; gives a 3 mm hole 24 sides and a large disc a few hundred, without slowing down tiny details.
Save $fn for shapes that need an exact count, like $fn = 6 for a hex nut. PrintPal's editor always does a full
render, so $preview is false there.
Strings: str(), len(), search() and friends
size = 3;
length = 10;
label = str("M", size, " x ", length);
echo(label); // "M3 x 10"
echo(len(label)); // 7
echo(label[0]); // "M"
echo(search("x", label)); // [3]
echo(ord("A"), chr(65)); // 65, "A"
Stock OpenSCAD has no split or substring function. BOSL2 adds substr(), str_split() and more.
Debug with echo() and assert()
wall = 1.2;
assert(wall >= 0.8, str("wall = ", wall, " mm is too thin to print"));
echo("wall =", wall); // ECHO: "wall =", 1.2
cube([20, 20, wall]);
A failed assert (2019.05+) stops the run and prints your message. It's the cleanest way to reject impossible parameter combinations.
include vs use: loading libraries like BOSL2
include <file.scad> | use <file.scad> | |
|---|---|---|
| Modules and functions | Yes | Yes |
| Top-level variables | Yes | No |
| Top-level geometry in the file | Drawn | Ignored |
| Typical use | BOSL2, shared constants | Libraries of modules, such as MCAD's boxes.scad |
include <BOSL2/std.scad>
cuboid([30, 20, 10], rounding = 2); // BOSL2 rounded box, centred on the origin
use <MCAD/boxes.scad>
roundedCube([30, 20, 10], 2, true, true); // size, radius, sides only, centred
BOSL2 needs OpenSCAD 2021.01 or later. In desktop OpenSCAD, put libraries in your library folder (File → Show Library Folder). In the PrintPal editor, BOSL2 and MCAD are built in and load as soon as your code includes them. More in OpenSCAD libraries.
Frequently asked questions
Can you change a variable in OpenSCAD?
Not after it's set. Each variable has one value per scope, and if you assign it twice the last assignment wins everywhere in that scope. Inside a loop, x = x + 1 creates a new local x on every pass. To accumulate a value, use a list comprehension or a recursive function.
How do you write a for loop in OpenSCAD?
for (x = [0 : 10 : 40]) translate([x, 0, 0]) cube(5); places a cube at 0, 10, 20, 30 and 40 mm. The range is [start : step : end] and includes the end when the steps land on it. You can also loop over a list of values, or give two loop variables to fill a grid.
Does OpenSCAD have else if?
Yes. Chain them as if (a) { … } else if (b) { … } else { … }. When you need to pick a value rather than geometry, use the ternary operator: h = a ? 10 : 5;
What is the difference between a function and a module in OpenSCAD?
A function returns a value (a number, string, list or another function) and is used inside expressions. A module produces geometry and is called as a statement. Only modules can receive child shapes through children().
Should I use include or use for BOSL2?
Use include <BOSL2/std.scad>. BOSL2 relies on constants defined at the top level of its files, and use skips top-level variables. Files that only define modules and functions, such as MCAD's boxes.scad, work fine with use.
Related articles
Further reading
Tools for thisAll tools
Watch it doneVideo library
Thanks! We read every note and use them to decide what to update next.