Skip to main content

🎨 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.

🧪 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.

📚 References

Release files for scadfmt 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for scadfmt 0.1.0
File Size Uploaded
scadfmt-0.1.0.tar.gz 24.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for scadfmt 0.1.0
File Interpreter ABI Platform
scadfmt-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 42.2 kB

Release files / scadfmt-0.1.0.tar.gz

Download URL scadfmt-0.1.0.tar.gz
Size 24.7 kB
Tags Source
SHA-256 checksum
How to use checksums
c023520d9817eb50564e2932e31522432291f7c71cc7ada141a4e8bdc869bf59
BLAKE2b-256 checksum
How to use checksums
e76799415fc6872975a237638fa37abbd71729beba9606b1e6d4be2225227500
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 27, 2026.

Transparency log

Release files / scadfmt-0.1.0-py3-none-any.whl

Download URL scadfmt-0.1.0-py3-none-any.whl
Size 17.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9dd3d7461307d8a8afa7392d9d450e33280e1a2844ed3385e99fa7c7825d9555
BLAKE2b-256 checksum
How to use checksums
7e562966beca490d0c0a4073f70d523de78825cd24c18d67182c7b8760805dd5
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 27, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page