🎨 scadfmt
📌 What
An opinionated formatter for OpenSCAD code. It fixes indentation and spacing, puts blocks and statements on their own lines, and never joins lines or changes what the code does.
🤔 Why
Community contributions need one code style without style debates in review. Existing formatters rewrite code they do not understand (dropping operators, breaking include paths) or join hand-wrapped lines into very long ones. scadfmt only needs to know OpenSCAD's tokens, so new syntax rarely affects it, and it refuses to write output whose tokens differ from the input. See build-own-openscad-formatter-scadfmt.
🔧 How
📦 Install
Requires Python 3.11 or newer, no other dependencies.
pip install scadfmt # from PyPI
pip install -e cmd/scadfmt # from this repo
▶️ Usage
scadfmt format models/ # format files in place, directories recursively
scadfmt format --check models/ # write nothing, exit 1 if a file would change
scadfmt format --diff part.scad # write nothing, print a diff
scadfmt format - < in.scad > out.scad # stdin to stdout
Exit codes: 0 clean, 1 files would change (--check), 2 error. On an error (unknown character, unbalanced brackets) the file stays untouched.
👀 Before and After
include<BOSL2/std.scad>
wall=2;// wall strength
height_units=3; // rack units
/**
* Bracket holding a device of the given size.
* center: centers the body on the origin
*/
module bracket(width=10,depth=20,center=false){
size=[width,depth,wall*height_units];
if(center){translate(-size/2)cube(size);}else{cube(size);}
for(i=[0:2:width])
translate([i,0,0])
rotate([0,0,-90])
#cylinder(h=wall,r=1);
}
becomes
include <BOSL2/std.scad>
wall = 2; // wall strength
height_units = 3; // rack units
/**
* Bracket holding a device of the given size.
* center: centers the body on the origin
*/
module bracket(width = 10, depth = 20, center = false) {
size = [width, depth, wall * height_units];
if (center) {
translate(-size / 2) cube(size);
} else {
cube(size);
}
for (i = [0:2:width])
translate([i, 0, 0])
rotate([0, 0, -90])
#cylinder(h = wall, r = 1);
}
📏 Rules
| Rule | Example |
|---|---|
2 spaces per open { ( [, one level per line that opens them |
cube([↵ 1,↵]); |
| A line continuing a module call nests one level further | translate(v)↵ cube(); |
| A line continuing an expression is one level in | x =↵ a +↵ b; |
Spaces around every binary operator and every = |
cube(size = w * 2, center = true); |
| Tight unary operators, modifiers, calls, indexing | -x, !a, #cube(), f(a)[0] |
| Tight range colons, spaced ternary colons | [0:2:10], a ? b : c |
if, for, intersection_for, function get a space before ( |
for (i = [0:2]) |
| Space after commas, none inside brackets | f(a, [1, 2]) |
Block contents on their own lines, } on its own line except } else, empty {} stays |
if (a) {↵ b();↵} else { |
One statement per line (; inside for (...) excepted) |
a();↵b(); |
Exactly one blank line before and after each module and function definition, none next to a brace |
x = 1;↵↵module m() { |
| Imports form one block without blank lines, followed by exactly one blank line | include <a.scad>↵use <b.scad>↵↵x = 1; |
| Trailing comments on consecutive lines share one column, a lone one gets 2 spaces | x = 1; // note |
| At most 2 blank lines at top level, 1 inside blocks | |
| Keeps the file's line endings (LF or CRLF, judged by the first one), no trailing whitespace, one final newline |
Comments (// or /* */) directly above a line belong to it, so a blank line added before that line goes above its comments. Lines are never joined and line length is never limited.
🙈 Opting Out
Lines between // fmt: off and // fmt: on stay as written, for example a hand-aligned matrix:
// fmt: off
identity = [
1, 0, 0,
0, 1, 0,
];
// fmt: on
🪝 Pre-commit
In another repository, install scadfmt from PyPI through a local hook:
- repo: local
hooks:
- id: scadfmt
name: scadfmt
entry: scadfmt format
language: python
additional_dependencies: [scadfmt==0.1.0]
files: \.scad$
🖥️ VS Code
scadfmt vscode
Installs the Custom Local Formatters extension and makes scadfmt the default formatter for .scad files in .vscode/settings.json, so Format Document (Shift+Alt+F) runs it. It refuses to touch a settings.json with comments; add the settings by hand then:
"customLocalFormatters.formatters": [{ "command": "\"/path/to/python\" -m scadfmt format -", "languages": ["scad"] }],
"[scad]": { "editor.defaultFormatter": "jkillian.custom-local-formatters" }
For formatting on save, add "editor.formatOnSave": true to the [scad] block.
🔄 Keeping Up with OpenSCAD
HomeRacker pins OpenSCAD nightly. When a weekly nightly bump changes OpenSCAD's grammar, a Claude agent adapts scadfmt and its canary on the Renovate PR, and a maintainer approves the result. See agent-adapts-scadfmt-to-openscad-nightly. To do the same by hand, follow the scadfmt-adapt skill.
🧪 Tests
See TESTING.md. tests/canary/ holds a file using every OpenSCAD construct and its expected output; check.sh checks both against the pinned OpenSCAD. tests/ast_check.sh proves that formatting-only changes in a PR keep OpenSCAD's AST.
📚 References
- #177: introduce an OpenSCAD formatter
- build-own-openscad-formatter-scadfmt: why scadfmt exists and how it works
- agent-adapts-scadfmt-to-openscad-nightly: how scadfmt follows new OpenSCAD nightlies
- OpenSCAD language reference
Release files for scadfmt 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| scadfmt-0.2.0.tar.gz | 27.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| scadfmt-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 45.6 kB
Release files / scadfmt-0.2.0.tar.gz
| Download URL | scadfmt-0.2.0.tar.gz |
|---|---|
| Size | 27.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
999a2c47714e4999a00c2ac7b60a20465abb498b2c5d0de8bdef58eee86cca3f
|
|
BLAKE2b-256 checksum How to use checksums |
6294c0134255bde31cbddbec0edbb9402f9f2e5cc582ee7ad67e81d2dfdf77c0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency logRelease files / scadfmt-0.2.0-py3-none-any.whl
| Download URL | scadfmt-0.2.0-py3-none-any.whl |
|---|---|
| Size | 18.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5a483767ce974b5d2a8029c2fffbdc2f1f4395f8ffd4baaec8db36751d7f593a
|
|
BLAKE2b-256 checksum How to use checksums |
e1c5fdb81cbf9930d5ce9d361be364447513540b8316fbe5a1938c9bfa362687
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency log