The uncompromising Python formatter.
Project description
True — The Uncompromising Python Formatter
True is an opinionated Python source-code formatter. It enforces a consistent style so you never have to think about formatting again.
Table of Contents
- Features
- Installation
- Quick Start
- CLI Reference
- API Reference
- PEP 8 Rules Applied
- Architecture
- Contributing
Features
| Category | What True fixes |
|---|---|
| Whitespace | Spaces around operators, after commas, inside brackets |
| Indentation | Any indent style → 4 spaces |
| Blank lines | 2 before top-level def/class, 1 between methods, 2 after last def |
| Imports | Split import os, sys into separate lines |
| Strings | Normalise 'hello' → "hello" |
| Semicolons | x = 0; y = 0 → two lines |
| Compound statements | if x: pass → split onto two lines |
| Lambda assignment | f = lambda x: x → def f(x): return x |
| Class names | class my_class → class MyClass |
| Variable names | xValue → x_value, calcArea → calc_area |
| Default params | def f(x =1) → def f(x=1) |
| Annotated params | def f(x: int=1) → def f(x: int = 1) |
| Keyword args | foo(key =1) → foo(key=1) |
| Power operator | 2 ** 10 → 2**10 |
| Extra spaces | for i in range → for i in range |
| Trailing whitespace | Stripped from every line |
| Final newline | Exactly one trailing newline |
Zero dependencies — pure stdlib.
Installation
pip install true-formatter
Or from source:
git clone https://github.com/yourname/true-formatter
cd true-formatter
pip install -e .[dev]
Quick Start
CLI
# Format a file in-place
true my_script.py
# Check without modifying (for CI)
true --check my_script.py
# Show a unified diff
true --diff my_script.py
# Format an entire directory
true src/
# Read from stdin
echo "x=1" | true -
Library
import true_formatter
code = "x=1\ny= 'hello'\n"
result = true_formatter.format_str(code, mode=true_formatter.Mode())
print(result)
# x = 1
# y = "hello"
CLI Reference
Synopsis
true [OPTIONS] [FILES|DIRECTORIES]...
Pass - as a file path to read from stdin and write to stdout.
Options
| Flag | Default | Description |
|---|---|---|
--check |
off | Don't write files; exit 1 if any would change |
--diff |
off | Print a unified diff instead of rewriting |
-l, --line-length INT |
88 |
Maximum line length |
-S, --skip-string-normalization |
off | Leave string quotes as-is |
-q, --quiet |
off | Suppress all non-error output |
Exit Codes
| Code | Meaning |
|---|---|
0 |
All files already formatted (or successfully reformatted) |
1 |
--check mode: at least one file would be reformatted |
Examples
# Format a single file
true app.py
# Format all Python files in a directory
true src/
# CI — fail if anything needs reformatting
true --check src/ tests/
# Preview changes before applying
true --diff app.py
# Pipe from stdin
cat messy.py | true - > clean.py
# Use a shorter line length
true -l 79 legacy_module.py
# Preserve existing string quotes
true -S my_file.py
Integration
pre-commit:
repos:
- repo: local
hooks:
- id: true-formatter
name: true
entry: true
language: python
types: [python]
Makefile:
fmt:
true src/ tests/
check:
true --check src/ tests/
GitHub Actions:
- name: Check formatting
run: |
pip install true-formatter
true --check src/ tests/
API Reference
Top-level Functions
format_str(src_contents, *, mode) → str
Format a Python source string and return the result.
import true_formatter
result = true_formatter.format_str("x=1", mode=true_formatter.Mode())
# → "x = 1\n"
| Parameter | Type | Description |
|---|---|---|
src_contents |
str |
Python source code to format |
mode |
Mode |
Formatting configuration |
Raises TypeError if src_contents is not a str.
Raises TrueFormattingError if the source cannot be tokenized.
format_file_contents(src_contents, *, mode) → str
Like format_str but additionally guarantees a single trailing newline.
Use this when writing back to a file.
formatted = true_formatter.format_file_contents(src, mode=mode)
Path("my_file.py").write_text(formatted, encoding="utf-8")
check_format(src_contents, *, mode) → bool
Return True if src_contents is already correctly formatted. No files are written.
ok = true_formatter.check_format("x = 1\n", mode=true_formatter.Mode())
# → True
ok = true_formatter.check_format("x=1\n", mode=true_formatter.Mode())
# → False
Mode
Dataclass that holds all formatting options.
from true_formatter import Mode
mode = Mode(
line_length=79,
string_normalization=True,
skip_string_normalization=False,
magic_trailing_comma=True,
)
| Field | Type | Default | Description |
|---|---|---|---|
line_length |
int |
88 |
Maximum line length |
string_normalization |
bool |
True |
Rewrite '…' as "…" |
skip_string_normalization |
bool |
False |
Disable string normalisation |
magic_trailing_comma |
bool |
True |
Preserve trailing commas |
target_versions |
set[TargetVersion] |
set() |
Python versions to target |
rules |
RuleSet |
DEFAULT_RULES |
Active rule set |
Properties:
mode.quote_char— returns'"'or"'"based on normalisation settings.
TargetVersion
Enum of supported Python versions.
from true_formatter import TargetVersion, Mode
mode = Mode(target_versions={TargetVersion.PY311, TargetVersion.PY312})
Values: PY38, PY39, PY310, PY311, PY312
Rules
Rule (abstract base class)
Extend this to create custom formatting rules that operate on the token stream.
import tokenize
from true_formatter import Rule, Mode
class BanPassRule(Rule):
name = "ban-pass"
def apply(self, tokens, mode):
return [t for t in tokens if t.string != "pass"]
| Attribute | Type | Description |
|---|---|---|
name |
str |
Unique identifier shown in --diff output |
Required method: apply(tokens, mode) → list[TokenInfo]
RuleSet
An ordered, immutable-style collection of rules.
from true_formatter import DEFAULT_RULES, Mode
from my_rules import BanPassRule
custom_rules = DEFAULT_RULES.add(BanPassRule())
mode = Mode(rules=custom_rules)
| Method | Returns | Description |
|---|---|---|
add(rule) |
RuleSet |
New set with rule appended |
remove(name) |
RuleSet |
New set without the named rule |
__len__() |
int |
Number of rules |
__iter__() |
iterator | Iterate over rules in order |
Exceptions
TrueError
├── TrueFormattingError ← source cannot be parsed / formatted
└── TrueConfigError ← invalid Mode configuration
from true_formatter.exceptions import TrueFormattingError
try:
true_formatter.format_str("def (:", mode=true_formatter.Mode())
except TrueFormattingError as e:
print(f"Bad source: {e}")
PEP 8 Rules Applied
| Code | Rule | Example |
|---|---|---|
| E101 | Indentation → 4 spaces | tab/2-space → 4 space |
| E201 | No space after ( [ { |
foo( x ) → foo(x) |
| E202 | No space before ) ] } |
foo(x ) → foo(x) |
| E203 | No space before , ; : |
x , y → x, y |
| E211 | No space before ( in call |
foo (x) → foo(x) |
| E225 | Spaces around operators | x==1 → x == 1 |
| E231 | Space after , : ; |
[1,2] → [1, 2] |
| E251 | No spaces around = in default params |
def f(x =1) → def f(x=1) |
| E252 | Spaces around = in annotated params |
def f(x: int=1) → def f(x: int = 1) |
| E261 | Two spaces before inline comment | x=1 # note → x = 1 # note |
| E262 | Space after # in comment |
#note → # note |
| E271 | Single space after keyword | for i → for i |
| E302 | Two blank lines before top-level def/class | |
| E301 | One blank line between methods in class | |
| E305 | Two blank lines after last def before code | |
| E401 | One import per line | import os, sys → two lines |
| E702 | No semicolons between statements | x=0; y=0 → two lines |
| E711 | Compound statements split onto separate lines | if x: pass → two lines |
| E731 | Lambda assignment → def | f = lambda x: x → def f(x): return x |
| W291 | No trailing whitespace | |
| W292 | File ends with newline | |
| N801 | Class names in PascalCase | class my_class → class MyClass |
| N802 | Function/variable names in snake_case | calcArea → calc_area |
Architecture
Formatting Pipeline
Source string
│
▼
Tokenizer (stdlib tokenize)
│
▼
Rule pipeline (true_formatter.rules)
│ each Rule: token list → token list
▼
Emitter (tokenize.untokenize)
│
▼
Text-level passes (true_formatter.transforms)
│
▼
Formatted string
Text-level Passes (in order)
| Pass | PEP 8 codes |
|---|---|
fix_indentation |
E101, W191 |
fix_multiple_imports |
E401 |
split_semicolons |
E702, E711 |
fix_lambda_assignment |
E731 |
fix_extra_whitespace |
E271, E272 |
fix_class_names |
N801 |
fix_variable_names |
N802 |
fix_pep8_whitespace |
E201, E202, E203, E211, E225, E231, E251, E252, E261, E262 |
fix_operator_priority_spacing |
E226 |
normalise_strings |
W605 |
fix_trailing_whitespace |
W291, W293 |
fix_blank_lines |
E302, E301 |
fix_blank_lines_after_def |
E305 |
ensure_final_newline |
W292 |
Module Map
true_formatter/
├── __init__.py Public API — re-exports from submodules
├── core.py format_str / format_file_contents / check_format
├── transforms.py Text-level formatting passes
├── rules.py Rule base class, RuleSet, built-in rules
├── exceptions.py TrueError hierarchy
└── cli.py argparse CLI entry point
Contributing
Setup
git clone https://github.com/yourname/true-formatter
cd true-formatter
pip install -e .[dev]
Running the Tests
pytest # all tests
pytest tests/test_transforms.py # one module
pytest -k "test_single" # filter by name
pytest --tb=short # compact tracebacks
Writing a New Rule
Rules live in true_formatter/rules.py and operate on the token stream:
import tokenize
from true_formatter.rules import Rule
class NoSemicolonRule(Rule):
name = "no-semicolons"
def apply(self, tokens, mode):
return [t for t in tokens if t.string != ";"]
Add it to DEFAULT_RULES and write tests in tests/test_rules.py.
Writing a New Text-level Transform
Transforms live in true_formatter/transforms.py and operate on strings:
def remove_semicolons(src: str) -> str:
return src.replace(";", "")
Wire it into _text_level_format in core.py in the correct position in the pipeline.
Test Guidelines
- One test class per module under test
- Each test method tests exactly one behaviour
- Use
pytest.raisesfor exception assertions - Use
tmp_pathfixture for file I/O tests - Name tests
test_<thing_being_tested>
Code Style
True is formatted with itself:
true true_formatter/ tests/
License
MIT © True Contributors
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file true_formatter-0.1.3.tar.gz.
File metadata
- Download URL: true_formatter-0.1.3.tar.gz
- Upload date:
- Size: 24.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3602117172370dc644210b195ca2afa9e5d218d069804ef07acfea0e728dbf70
|
|
| MD5 |
abc44be4026cb7439fa6ab15e341e7e0
|
|
| BLAKE2b-256 |
c2aeb50c353ac12b1f04e9bd3553084ea0c0a048f165040d52766d22f06da302
|
File details
Details for the file true_formatter-0.1.3-py3-none-any.whl.
File metadata
- Download URL: true_formatter-0.1.3-py3-none-any.whl
- Upload date:
- Size: 15.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
81eeb6065c938ee9f9e78b7aeb185ff3fe8065f5a511f50b0b30e7c341ffe765
|
|
| MD5 |
98634ff978b3f334c4859fc367d6852c
|
|
| BLAKE2b-256 |
5eff6e5032cda62cf867047d09285003aabe5aa3a27a2e5beb053347a7915ac4
|