OpenSCAD modules and functions: children(), let() and use vs include
An OpenSCAD module is a reusable piece of geometry that you call like a built-in shape. A function is a reusable calculation that returns a value. Modules draw, functions compute, and together they turn a long script into a small, readable part library.
module name(args) { … }makes geometry;function name(args) = expr;returns a value.- Give every argument a sensible default and call with named arguments.
children()lets a module wrap, move or repeat whatever you put after it.- Use
includefor BOSL2 and for files whose variables you need;useimports only modules and functions.
Modules vs functions at a glance
| Module | Function | |
|---|---|---|
| Defined with | module m(a=1) { … } | function f(a=1) = a * 2; |
| Produces | Geometry | A value: number, vector, list, string or another function |
| Called as | A statement: m(a=2); | Part of an expression: x = f(2); |
| Body | Any statements, loops and if | One expression; use let(), ?:, list comprehensions or recursion |
Can take children() | Yes | No |
Define a module with default arguments
module washer(od=10, id=3.2, t=1.6) {
difference() {
cylinder(h=t, d=od, $fn=48);
translate([0, 0, -1]) cylinder(h=t + 2, d=id, $fn=32);
}
}
washer(); // all defaults
translate([20, 0, 0]) washer(od=20, id=8, t=3); // named arguments
translate([45, 0, 0]) washer(16, 5); // positional: od, id
Arguments can be passed by position or by name, and any you leave out take their default. Named arguments are clearer and survive reordering, so prefer them for anything past the first one or two.
A misspelled argument only warns. washer(thickness=3) runs with the default t and prints a warning that thickness is not a parameter. If a change seems to do nothing, check the console.
children(): modules that wrap other shapes
A module can act like a transformation. Whatever you write after the call is available
inside it as children():
module array_x(n=5, spacing=10) {
for (i = [0:n - 1])
translate([i * spacing, 0, 0]) children();
}
array_x(6, 12) cylinder(h=8, d=6, $fn=32);
| Form | Draws |
|---|---|
children() | All children |
children(i) | Child number i, counting from 0 |
children([0:2]) | A range of children |
children([0, 3]) | A list of chosen children |
$children | Not a call: the number of children passed |
Pass several children in braces and place each one separately:
module row(spacing=15) {
for (i = [0:1:$children - 1]) // the explicit step stops [0:-1] when empty
translate([i * spacing, 0, 0]) children(i);
}
row() {
cube(8);
translate([4, 4, 4]) sphere(d=8, $fn=32);
translate([4, 4, 0]) cylinder(h=8, d=8, $fn=32);
}
Using array_x from above, how many shapes does array_x(3, 10) { cube(2); sphere(1); } draw?
children() with no index draws every child, so each of the 3 loop passes draws both the cube and the sphere: 6 shapes.
Special variables like $fn flow into modules
Variables that start with $ are passed down to everything a call reaches,
including shapes inside your modules. Setting $fn on the call changes the
resolution of the whole module without adding a parameter:
module post() { cylinder(h=10, d=8); }
post($fn=6); // hexagonal
translate([15, 0, 0]) post($fn=64); // smooth: $fn flows into the module
Functions return values
A function body is a single expression. Use them for unit conversions, derived
dimensions and point lists for polygon():
function in_to_mm(i) = i * 25.4;
function circle_pts(r, n=36) =
[for (i = [0:n - 1]) [r * cos(i * 360 / n), r * sin(i * 360 / n)]];
linear_extrude(height=3) polygon(circle_pts(20, 8)); // an octagon plate
translate([50, 0, 0]) cube([in_to_mm(1), in_to_mm(0.5), 3]); // 25.4 × 12.7 mm
A bare polygon() is 2D. The PrintPal editor only exports 3D solids, so wrap
2D shapes in linear_extrude() as above; see
2D shapes and extrusion.
let(): local names inside a function
function diagonal(w, h) = let (w2 = w * w, h2 = h * h) sqrt(w2 + h2);
echo(diagonal(30, 40)); // ECHO: 50
cube([30, 40, 2]);
List comprehensions: loops that build lists
Functions cannot contain a for statement, but a list comprehension does the
same job and returns a list. circle_pts() above is one. You can filter with
if, as in [for (x = v) if (x > 0) x], and the C-style form
carries a running value from one step to the next:
heights = [5, 10, 3, 8];
// Running total with a C-style list comprehension: the z position of each layer
function z_offsets(h) = [for (i = 0, z = 0; i < len(h); z = z + h[i], i = i + 1) z];
z = z_offsets(heights); // [0, 5, 15, 18]
echo(z);
for (i = [0:len(heights) - 1])
translate([0, 0, z[i]]) cylinder(h=heights[i], d=30 - 5 * i, $fn=48);
Function literals (OpenSCAD 2021.01 and later)
Since OpenSCAD 2021.01 a function can be stored in a variable and passed to another
function, which makes small helpers like map_list() possible:
double = function (x) x * 2;
function map_list(v, f) = [for (x = v) f(x)];
sizes = map_list([2, 4, 6], double); // [4, 8, 12]
echo(sizes);
for (i = [0:len(sizes) - 1])
translate([i * 15, 0, 0]) cube(sizes[i]);
Recursion and its limits
Recursion is the other way to loop in a function. OpenSCAD has two limits to know about:
| Kind | Limit |
|---|---|
Plain recursion, e.g. n + sum_to(n - 1) | A few thousand levels, depending on the build |
| Tail recursion: the recursive call is the whole result of one branch | 1,000,000 steps, because OpenSCAD turns it into a loop |
| Recursive modules | No loop conversion, so keep the depth modest |
Going past a limit stops the run with Recursion detected calling function
(or calling module). If you see it, switch to the tail-recursive form or a
list comprehension.
// Plain recursion: fine for small inputs
function sum_to(n) = n == 0 ? 0 : n + sum_to(n - 1);
// Tail recursion: the recursive call is the last step,
// so OpenSCAD turns it into a loop
function sum_to_tail(n, acc=0) = n == 0 ? acc : sum_to_tail(n - 1, acc + n);
echo(sum_to(100)); // ECHO: 5050
echo(sum_to_tail(100000)); // ECHO: 5.00005e+9
cube(5);
Recursive modules are handy for self-similar shapes:
module tower(levels, d=30, h=6) {
cylinder(h=h, d=d, $fn=48);
if (levels > 1)
translate([0, 0, h]) tower(levels - 1, d * 0.8, h);
}
tower(5); // five tiers, each 80% of the one below
Group related settings in a list
When a module needs many values, a named list keeps presets together. The trade-off is
that p[3] is less readable than hole_d, so comment the order:
// [length, width, thickness, hole_d, hole_inset]
M3_BRACKET = [30, 20, 4, 3.4, 5];
module bracket(p) {
difference() {
cube([p[0], p[1], p[2]]);
translate([p[4], p[1] / 2, -1])
cylinder(h=p[2] + 2, d=p[3], $fn=24);
}
}
bracket(M3_BRACKET);
use vs include: loading library files
include <file.scad> | use <file.scad> | |
|---|---|---|
| Modules and functions | Imported | Imported |
| The file's variables and constants | Imported, and your file can override them | Not visible to your file |
| The file's top-level shapes | Drawn | Skipped |
| Where it can go | Anywhere | Top level only |
| Good for | BOSL2, constants files, your own shared settings | MCAD, files with demo geometry at the bottom |
With use, a library's modules work but its variables do not. MCAD's
nuts_and_bolts.scad defines a constant MM; through
use it is unknown, while boltHole() still works:
use <MCAD/nuts_and_bolts.scad>
echo(MM); // undef, with a warning: use does not import variables
difference() {
cube([20, 20, 6]);
translate([10, 10, -1]) boltHole(3, length=10); // modules do come through
}
Load BOSL2 with include. use <BOSL2/std.scad> fails, because BOSL2's modules depend on constants such as BOT and TOP that use leaves behind. See OpenSCAD libraries for what is bundled in the PrintPal editor.
With include, a variable set in your own file wins over the library's value
silently, which is useful for settings and confusing if a name clashes by accident.
Name features for the CAD Agent with @feature
In the PrintPal editor, a // @feature: name comment on the line above a
module call names that piece of the model. The editor turns each tag into a
chip: click it to highlight the feature in 3D and drop @feature[name]
into the chat, so you can write "make @feature[corner_holes] 4 mm" instead of describing
where the holes are. Tags placed above a module definition highlight nothing,
because a definition draws nothing on its own.
plate_length = 60;
plate_width = 30;
plate_thickness = 4;
hole_d = 3.4;
hole_inset = 6;
module plate() { cube([plate_length, plate_width, plate_thickness]); }
module corner_holes() {
for (x = [hole_inset, plate_length - hole_inset],
y = [hole_inset, plate_width - hole_inset])
translate([x, y, -1]) cylinder(h=plate_thickness + 2, d=hole_d, $fn=24);
}
difference() {
// @feature: base_plate
plate();
// @feature: corner_holes
corner_holes();
}
The CAD Agent guide covers the chips and edit workflow, and the parametric design guide shows how to lay out a whole file this way.
Ask the CAD Agent for reusable modules
Write a reusable module polar_array(n, radius) that places its children() n times around a circle. Use it to make a 70 mm round desk organiser, 30 mm tall, with six 12 mm pen holes 20 mm deep on a 50 mm circle. Keep all sizes as parameters at the top.
Refactor my current design: one module per feature with default arguments, parameters at the top, a // @feature tag above each module call, and a single top-level call that builds the part.
Frequently asked questions
What is the difference between a module and a function in OpenSCAD?
A module creates geometry and is called as a statement, like cube(). A function returns a value such as a number, vector or list, and is used inside expressions. Functions cannot draw shapes, and modules cannot return values.
What does children() do in OpenSCAD?
children() draws the shapes written after a module call, so your module can move, repeat or cut them like a built-in transformation. children(i) draws only child number i, counting from 0, and $children holds how many children were passed.
What is the difference between use and include in OpenSCAD?
include pastes the whole file in, so its variables, modules, functions and top-level shapes all come along. use imports only modules and functions, skips the file's top-level shapes and keeps its variables private. BOSL2 must be loaded with include.
Can an OpenSCAD function contain a loop?
Not a for statement, because a function body is a single expression. Use a list comprehension such as [for (i = [0:n-1]) i * 2], or recursion. Tail-recursive functions are turned into loops and can run up to a million steps.
Can I store a function in a variable in OpenSCAD?
Yes, since OpenSCAD 2021.01. Write f = function (x) x * 2; then call it as f(3) or pass f to another function as an argument.
Sources & further reading
- OpenSCAD User Manual — User-defined functions and modules (children, function literals, recursion limits)
- OpenSCAD User Manual — Include statement (use vs include)
- OpenSCAD User Manual — Libraries (library search paths)
Tools for thisAll tools
Watch it doneVideo library
Thanks! We read every note and use them to decide what to update next.