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 (of --workspace <folder>, default .), so Format Document (Shift+Alt+F) runs it.

⚠️ In a multi-root workspace, VS Code ignores the formatter list in a folder's .vscode/settings.json. Pass the .code-workspace file to --workspace there, so the settings go into it instead:

scadfmt vscode --workspace                        # asks about the nearest .code-workspace file in . or above
scadfmt vscode --workspace ../my.code-workspace   # no prompt

Declining the offered file, or having none, asks for a workspace file or folder.

It refuses to touch a settings file with comments; add the settings by hand then (in a .code-workspace file, inside its "settings" block):

"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

Metadata

Release files for scadfmt 0.3.1

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.3.1
File Size Uploaded Provenance Actions
scadfmt-0.3.1.tar.gz 30.1 kB With provenance Details for scadfmt-0.3.1.tar.gz

Built distribution (wheel)

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

Total release size: 49.5 kB

Release files / scadfmt-0.3.1.tar.gz

Download URL scadfmt-0.3.1.tar.gz
Size 30.1 kB
Tags Source
SHA-256 checksum
How to use checksums
5b714f8f87eb8d0a7de7f1ba676b9541a4f5e8d711d4b2c7590e514fdd04e93e
BLAKE2b-256 checksum
How to use checksums
c3a5660644e3059b17b01f4005e754e44ff65d3c192c358b3bb250f807068212
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Nov 2, 2026.

Transparency log

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

Download URL scadfmt-0.3.1-py3-none-any.whl
Size 19.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
41ccb96d8e8589f9e8a2fded039e5b31d40570447807bad6772ddd1596cce025
BLAKE2b-256 checksum
How to use checksums
ffa3b187c78822f791905a04a0a7dd5247b3959c87b9e2cb35d030672d96587f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Nov 2, 2026.

Transparency log

Security for scadfmt 0.3.1

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.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for scadfmt-0.3.1.tar.gz

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 Nov 2, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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