A fast, HTML aware, Django template formatter, written in Rust.
Heavily rely on the awesome markup_fmt with some additions to support Django fully.
djangofmt is available on PyPI.
# With pip
pip install djangofmt
# With uv
uv tool install djangofmt@latest # Install djangofmt globally.
uv add --dev djangofmt # Or add djangofmt to your project.
# With pipx
pipx install djangofmt
See pre-commit for instructions
Sample .pre-commit-config.yaml
:
- repo: https://github.com/UnknownPlatypus/djangofmt-pre-commit
rev: v0.1.0
hooks:
- id: djangofmt
The separate repository enables installation without compiling the Rust code.
By default, the configuration uses pre-commit’s files
option to detect
all text files in directories named templates
. If your templates are stored elsewhere, you can override this behavior
by specifying the desired files in the hook configuration within your .pre-commit-config.yaml
file.
Usage: djangofmt [OPTIONS] <FILES>...
Arguments:
<FILES>...
List of files to format
Options:
--line-length <LINE_LENGTH>
Set the line-length [default: 120]
--profile <PROFILE>
Template language profile to use [default: django] [possible values: django, jinja]
--custom-blocks <BLOCK_NAMES>
Comma-separated list of custom block name to enable
-h, --help
Print help
-V, --version
Print version
Djangofmt does not have any ability to recurse through directories. Use the pre-commit integration, globbing, or another technique to apply it to many files. For example, git ls-files | xargs:
git ls-files -z -- '*.html' | xargs -0r djangofmt
…or PowerShell’s ForEach-Object
:
git ls-files -- '*.html' | %{djangofmt $_}
DjangoFmt gives users control over formatting in cases where static analysis struggles to determine the optimal approach.
You can control this formatting by choosing whether to insert a newline before the first attribute:
# Unchanged
<div class="flex" id="great" data-a>
This is nice!
</div>
# Wrap on multiple lines
<div
- class="flex" id="great" data-a>
+ class="flex"
+ id="great"
+ data-a
+>
This is nice!
</div>
The class
attribute will be formatted as a space-separated sequence of strings,
unless there are already newlines inside the attribute value.
This makes it possible to accommodate the 2 following use cases:
<div class="
mt-8 p-8
bg-indigo-600 hover:bg-indigo-700
border border-transparent
font-medium text-white
">
Hello world
</div>
<div class="mt-8 p-8 bg-indigo-600 hover:bg-indigo-700 border border-transparent font-medium text-white">
Hello world
</div>
See g-plane/markup_fmt#75 (comment) for the rationale.
All of these could be solved but were not prioritized for now.
The style
attribute will be formatted using a CSS formatter (Malva),
but the output will always be on a single line.
Before:
<div class="flex flex-col items-center absolute z-10"
style="top:60%;
transform:translate(0,-50%)">
Such a lovely day
</div>
After:
<div class="flex flex-col items-center absolute z-10"
style="top:60%; transform:translate(0,-50%)">
Such a lovely day
</div>
Here are the results benchmarking djangofmt
against similar tools on 100k lines of HTML across 1.7k files.
Formatting 100k+ lines of HTML across 1.7k+ files from scratch.
This is important to note that only djlint
covers the same scope in terms of formatting capabilities.
djade
only alter django templating, djhtml
only fix indentation and prettier
only understand html (and will break templates)
As always, these results should be taken with a grain of salt. Results on my machine will differ from yours, especially if you have many CPU cores because some tools take better advantage of parallelization than others.
But at least it was fun to build thanks to the wonderful hyperfine tool.
Benchmark details (2025-02-28)
This was run on my AMD Ryzen 9 7950X (32) @ 5.881GHz.
Tools versions:
- djangofmt: v0.1.0
- prettier: v3.5.2
- djlint: v1.36.4
- djade: v1.3.2
- djhtml: v3.0.7
Benchmark 1: cat /tmp/test-files | xargs --max-procs=0 ../../target/release/djangofmt format --profile django --line-length 120 --quiet
Time (mean ± σ): 19.8 ms ± 0.9 ms [User: 179.6 ms, System: 73.7 ms]
Range (min … max): 18.3 ms … 23.3 ms 73 runs
Warning: Ignoring non-zero exit code.
Benchmark 2: cat /tmp/test-files | xargs --max-procs=0 djade --target-version 5.1
Time (mean ± σ): 72.0 ms ± 1.0 ms [User: 63.2 ms, System: 9.3 ms]
Range (min … max): 70.5 ms … 73.4 ms 18 runs
Benchmark 3: cat /tmp/test-files | xargs --max-procs=0 djhtml
Time (mean ± σ): 1.401 s ± 0.026 s [User: 1.322 s, System: 0.079 s]
Range (min … max): 1.373 s … 1.453 s 10 runs
Benchmark 4: cat /tmp/test-files | xargs --max-procs=0 djlint --reformat --profile=django --max-line-length 120
Time (mean ± σ): 2.343 s ± 0.026 s [User: 64.944 s, System: 1.176 s]
Range (min … max): 2.297 s … 2.377 s 10 runs
Warning: Ignoring non-zero exit code.
Benchmark 5: cat /tmp/test-files | xargs --max-procs=0 ./node_modules/.bin/prettier --ignore-unknown --write --print-width 120 --log-level silent
Time (mean ± σ): 3.226 s ± 0.062 s [User: 4.481 s, System: 0.261 s]
Range (min … max): 3.092 s … 3.292 s 10 runs
Warning: Ignoring non-zero exit code.
Summary
cat /tmp/test-files | xargs --max-procs=0 ../../target/release/djangofmt format --profile django --line-length 120 --quiet ran
3.63 ± 0.17 times faster than cat /tmp/test-files | xargs --max-procs=0 djade --target-version 5.1
70.71 ± 3.45 times faster than cat /tmp/test-files | xargs --max-procs=0 djhtml
118.28 ± 5.48 times faster than cat /tmp/test-files | xargs --max-procs=0 djlint --reformat --profile=django --max-line-length 120
162.80 ± 7.96 times faster than cat /tmp/test-files | xargs --max-procs=0 ./node_modules/.bin/prettier --ignore-unknown --write --print-width 120 --log-level silent
You can generate shell completions for your preferred
shell using the djangofmt completions
command.
Usage: djangofmt completions <SHELL>
Arguments:
<SHELL>
The shell to generate the completions for
[possible values: bash, elvish, fish, nushell, powershell, zsh]