Reference

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.

7 min read Updated October 2026 Beginner OpenSCAD reference · 2 of 8
In short
  • Statements end with ;. A transform, boolean or if applies to the next statement or { } block.
  • Seven types: number, boolean, string, list, range, undef and function.
  • Variables can't be changed. for makes geometry, and list comprehensions make lists.
  • if chooses geometry, and cond ? a : b chooses values.
  • Load BOSL2 with include. Use use for 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

TypeExamplesTest
Number3, -2.5, 1e6, PIis_num()
Booleantrue, falseis_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)
Undefinedundefis_undef()
Function (2021.01+)function (x) x * 2is_function()
Falsy values are false, 0, "", [] and undef. Everything else is true, including the string "0".
Mixing types doesn't convert anything. "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;

  1. 20
  2. 50
  3. 20, then 50
  4. An error

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
Count down with a negative step, not [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;

Variables set inside 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);
C-style loops only exist inside list comprehensions.

[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?

  1. [0, 2, 4]
  2. [0, 2, 4, 6]
  3. [2, 4, 6]
  4. The range [0 : 2 : 6] unchanged

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

FunctionModule
Definitionfunction name(a, b = 1) = expr;module name(a, b = 1) { … }
ProducesA valueGeometry
CalledInside an expressionAs a statement
Takes child shapesNoYes, via children()
Can recurseYes (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
VariableDefaultPurpose
$fn0Exact segment count when above 0. Overrides $fa/$fs
$fa / $fs12° / 2 mmMinimum angle and minimum length per segment
$t0Animation time, from 0 up to but never reaching 1
$previewn/atrue in F5 preview, false in an F6 render or export (2019.05+)
$childrenn/aNumber of children passed to the current module
$vpr $vpt $vpd $vpfn/aViewport rotation, translation, distance and field of view ($vpf 2021.01+)
Use $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 functionsYesYes
Top-level variablesYesNo
Top-level geometry in the fileDrawnIgnored
Typical useBOSL2, shared constantsLibraries 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