spacify
A fast formatter for the vertical rhythm of Python and Rust code. ruff format and rustfmt decide
how a statement is laid out, but leave most of the blank lines between statements up to you. spacify
fills that gap: it puts air around logical blocks and tightens runs of related statements, and it
only touches the blank lines between statements.
Before:
def load(paths):
seen = set()
for path in paths:
if path in seen:
continue
seen.add(path)
total = len(seen)
count = 0
log(total)
return total
After spacify:
def load(paths):
seen = set()
for path in paths:
if path in seen:
continue
seen.add(path)
total = len(seen)
count = 0
log(total)
return total
spacify never fights your formatter. Every rule stays inside the blank-line bounds ruff format and
rustfmt keep, and CI runs spacify and the formatter over CPython's standard library, ripgrep and
tokio in both orders to check they reach a fixed point. It also checks that spacify leaves every
Python file's AST, and every Rust file's tokens, unchanged. Checking CPython's Lib, without its
tests, takes under half a second on 8 cores.
Install
uv tool install spacify # or: pipx install spacify, pip install spacify
Use with prek or pre-commit
Put the hook after your formatter's (ruff-format, or a cargo fmt hook), so spacify sees its
line wrapping:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.9
hooks:
- id: ruff-format
- repo: https://github.com/avanserv/spacify-pre-commit
rev: v0.1.0
hooks:
- id: spacify
The hook installs the prebuilt wheel from PyPI, so you don't need a Rust toolchain.
Command line
spacify # format every Python and Rust file under the current directory
spacify src/ tests/a.py # format these paths
spacify --check # write nothing; exit 1 if any file would change
spacify --diff # write nothing; print a unified diff
spacify - < a.py # read stdin, write stdout (--stdin-filename names it)
The exit code is 0 on success, 1 when --check or --diff found changes, and 2 on an error such as
a syntax error, in which case the file is left untouched. Directory walks respect .gitignore and
skip the directories ruff skips by default. A file you name explicitly is always formatted, unless
you also pass --force-exclude, which is what the hook does.
Languages
spacify formats every language it supports by default: Python (.py, .pyi) and Rust (.rs). To
format only some, list them in the configuration, for example languages = ["python"]. Files in
other languages are skipped, even when you name them, and stdin takes its language from
--stdin-filename (Python without one).
Known limits
In Python, spacify parses with tree-sitter-python, which rejects a few valid constructs: type parameter
defaults (def f[T = int](), PEP 696), a parenthesised starred subscript (a[*(b)]), and an
attribute access broken after its dot onto a line indented less than the statement. A file using one
exits 2 and is left untouched. Files must be UTF-8, as for ruff, and line endings that are a bare
\r are refused the same way. Python 2 print and exec statements are not rejected, since
tree-sitter cannot tell some of them from valid Python 3 such as print >>f, x.
In Rust, tree-sitter-rust refuses some nightly-only syntax, and such a file exits 2 untouched.
spacify assumes rustfmt's default ceiling of one blank line, so it does not support a
blank_lines_upper_bound above 1.
Rules
Each rule can be turned on or off. They all apply to sibling statements in the same block.
| Rule | Effect |
|---|---|
blank-before-block |
At least one blank line before a block statement when another statement comes before it. |
blank-after-block |
At least one blank line after a block statement, before the next statement. |
blank-before-exit |
At least one blank line before an exit, in blocks of exit-min-block-statements (default 3) or more statements. |
collapse-groups |
No blank line between two single-line statements of the same kind. |
The added blank lines take priority over collapse-groups. Blank lines that have a comment in the
gap are kept, because a comment marks a paragraph you meant. When a rule does add a blank line, it
goes above the comments and attributes that belong to the next statement. What each term means
depends on the language:
| Python | Rust | |
|---|---|---|
| Block statements | if, for, while, try, with, match, and their async forms |
if, match, for, while, loop, and bare, unsafe, async and const blocks |
| Exits | return, raise, break, continue, a bare yield |
return, break, continue, and a function body's tail expression |
| Kinds that collapse | assignments, augmented assignments, calls, del, assert, global/nonlocal, module constants |
let and assignments, compound assignments, calls (macros, .await and ? included) |
| Never touched | gaps next to a def or class, after an import, after a docstring |
gaps next to an item (fn, struct, use, const, ...) |
| Switched off by | # fmt: off/# fmt: on, # spacify: off/# spacify: on, # fmt: skip |
// spacify: off/// spacify: on, #[rustfmt::skip] |
| Most blank lines in a row | 2 at module level (1 in a .pyi stub), 1 in a nested block |
1 |
In Python, an assignment outside every function and class whose target names all have an uppercase
letter and no lowercase one (MAX = 3, T = TypeVar("T"), A, *REST = items) is a constant, a kind
apart from other assignments. Imports are never collapsed, because ruff's isort rules separate
import sections with blank lines. In Rust, the tail expression of a closure, a match arm or a
let x = { ... } block is a value rather than a return, so it stays tight.
Configuration
spacify reads [tool.spacify] from pyproject.toml, or the top level of a spacify.toml or
.spacify.toml, which is the place for a Rust project. It uses the first one it finds walking up from the current directory, or the file
you pass with --config.
[tool.spacify]
extend-exclude = ["generated", "migrations/*.py"] # `exclude = [...]` replaces the defaults
languages = ["python", "rust"] # the default: every supported language
[tool.spacify.rules]
blank-before-block = true
blank-after-block = true
blank-before-exit = true
exit-min-block-statements = 3
collapse-groups = true
[tool.spacify.rust.rules] # overrides the shared rules for Rust only
exit-min-block-statements = 4
An exclude pattern containing a / matches the path relative to the config file. Any other pattern
matches a single file or directory name.
Contributing
See CONTRIBUTING.md.
Metadata
Release files for spacify 0.3.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 | Provenance | Actions |
|---|---|---|---|---|
| spacify-0.3.0.tar.gz | 34.8 kB | Without provenance | Details for spacify-0.3.0.tar.gz |
Built distributions (wheels)
Total release size: 9.6 MB
Release files / spacify-0.3.0.tar.gz
| Download URL | spacify-0.3.0.tar.gz |
|---|---|
| Size | 34.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
026be9e472d575fa474405a5e62c82f8df95cba86d686222119b47cc168de7d0
|
|
BLAKE2b-256 checksum How to use checksums |
2e4822ff42e2d3a5522ccb5b9c5a7392f2772e5cd994bb12c3348ed6cd7fd38a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / spacify-0.3.0-py3-none-win_amd64.whl
| Download URL | spacify-0.3.0-py3-none-win_amd64.whl |
|---|---|
| Size | 1.3 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
33dc0c315bf5ea15d4cf3bf57424e1d09760d8156ec7a7b3dc3a7f1f1b25852b
|
|
BLAKE2b-256 checksum How to use checksums |
293058c60d2cb593317845f17d74cbb1017a9b5ec7100d97c61760e6ee02daec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / spacify-0.3.0-py3-none-musllinux_1_2_x86_64.whl
| Download URL | spacify-0.3.0-py3-none-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 1.5 MB |
| Tags | Linux musl 1.2+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
c1398af610ab9f9e5a3d782221b3cb683bded74f2b3d59c96631137c305548d2
|
|
BLAKE2b-256 checksum How to use checksums |
269e18eb2df486c799f519399de8c2db683205d22475b1e83a22ec51569b2513
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / spacify-0.3.0-py3-none-musllinux_1_2_aarch64.whl
| Download URL | spacify-0.3.0-py3-none-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 1.4 MB |
| Tags | Linux musl 1.2+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
3ea285df6891922ed5c64b4fdcdc58ab4121fcd004ddf142fbf0cf4c98690aa9
|
|
BLAKE2b-256 checksum How to use checksums |
beb90cf76c2cf68448780106ba76d47bc76efc7498085a276d89d864fe3501ed
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / spacify-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | spacify-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 1.4 MB |
| Tags | Linux glibc 2.17+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
e8e4f085084e481918a4ece635a103cce0ebef2df05b88f224bf3fc58b8806ab
|
|
BLAKE2b-256 checksum How to use checksums |
4e80411302564acbc3ac5208f182d21fb5457a0d59d0bc8b060afda6d74c4f12
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / spacify-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | spacify-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 1.4 MB |
| Tags | Linux glibc 2.17+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
cfb0df253aade8e7b0c37106bb41fb9b0e226bf72b9be483502572192b688663
|
|
BLAKE2b-256 checksum How to use checksums |
1cdcc06cb7fe4cf88ac1220b0dc53338c6f53860b69a75fafd9f99f7d065d290
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / spacify-0.3.0-py3-none-macosx_11_0_arm64.whl
| Download URL | spacify-0.3.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 1.3 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
8dec8c458ac540b706509b5f5cf05b280f5ffe8c09019435c5321d6d16db0464
|
|
BLAKE2b-256 checksum How to use checksums |
dd237e91ad21bcb1f4bb6ae64106bb33dc547641f99143aa1499d34773681979
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / spacify-0.3.0-py3-none-macosx_10_12_x86_64.whl
| Download URL | spacify-0.3.0-py3-none-macosx_10_12_x86_64.whl |
|---|---|
| Size | 1.4 MB |
| Tags | Python 3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
d978184e90ed4b6960dc937ce896f8a78cf7749b560c953504eefc4c4eb4f0a4
|
|
BLAKE2b-256 checksum How to use checksums |
dc8005cd5ead36c72e5c377dafffc908ed4c0465a00731eb5187dc6e32e5896d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for spacify 0.3.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
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.