Skip to main content

🎨 scadfmt

PyPI Pre-commit Coverage Mutation score

📌 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

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)

Source distribution for scadfmt 0.2.0
File Size Uploaded
scadfmt-0.2.0.tar.gz 27.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for scadfmt 0.2.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

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