tailor
An opinionated Python formatter: ruff format, then a house style.
tailor runs ruff format, then puts one item on each line, spaces keyword arguments, groups the code with blank lines and sorts imports by length. It also lays out the SQL in """--sql strings.
Example
Before:
import structlog
from jobs.models import JobAborted
import asyncio
class Builder:
async def create(self, *, user_request: str, title: str, milestone: str, next_title: str) -> Response:
prediction = await self.orchestration.predict(self.anchor.acall, user_request=user_request)
numbers = [number for number in bundle.steps if 1 <= number <= len(steps) and number not in claimed]
return Node(label=self.state.next_label(), name=card.name, keywords=set(card.keywords))
After:
import asyncio
import structlog
from jobs.models import JobAborted
class Builder:
async def create(
self,
*,
user_request: str,
title: str,
milestone: str,
next_title: str,
) -> Response:
prediction = await self.orchestration.predict(
self.anchor.acall,
user_request = user_request,
)
numbers = [
number
for number in bundle.steps
if 1 <= number <= len(steps) and number not in claimed
]
return Node(
label = self.state.next_label(),
name = card.name,
keywords = set(card.keywords),
)
Installation
uv tool install pytailor
tailor runs on Python 3.14, and uv gets it for you. It formats projects that target Python 3.12 to 3.14. It reads the target the same way ruff does. If a project targets another version, tailor stops before it changes a file. For a project that targets Python 3.12 or 3.13, ruff adds from __future__ import annotations to each file.
Usage
tailor src tests # format in place
tailor --check src # list the files that would change
tailor --diff src # show the changes as a colored diff
tailor --line-length 100 src # replace the line length for this run
tailor formats the .py files that ruff check finds: ruff's default exclusions, exclude and extend-exclude in [tool.ruff], and .gitignore apply. The excludes of [tool.ruff.lint] and [tool.ruff.format] do not apply. With --check or --diff, the exit code is 1 when a file would change, so tailor --check src tests works as a CI step.
To format on save in Neovim, add tailor to conform.nvim:
require("conform").setup({
formatters = {
tailor = { command = "tailor", args = { "$FILENAME" }, stdin = false },
},
formatters_by_ft = { python = { "tailor" } },
format_on_save = { timeout_ms = 1000 },
})
Configuration
Put the options in [tool.tailor] of pyproject.toml. tailor uses the nearest pyproject.toml above each file. An unknown option or a wrong type stops tailor with an error that names the file.
| Option | Default | What it sets |
|---|---|---|
line-length |
ruff's line-length, else 88 |
The maximum line length. |
module-definition-blank-lines |
2 | Blank lines around top-level functions and classes. |
class-definition-blank-lines |
2 | Blank lines around methods and nested classes. |
compact-class-lines |
5 | A class this short has no blank lines between its attributes. |
assignment-group-size |
3 | The group size for a long run of assignments. |
long-body-lines |
2 | A for, while or if with a longer body gets a blank line after it. |
short-dunder-lines |
2 | Dunders this short have 1 blank line between them. |
constructors |
__new__, __init__, __post_init__ |
The dunders that come first in a class. |
sql-dialect |
found for each file | The dialect of --sql strings, for example "postgres". |
sql-indent-width |
2 | The indent of a SQL clause or subquery. |
House style
The numbers below are the defaults.
- Line breaks. A call, definition, import or literal that does not fit puts each item on its own line. A long comprehension puts each
forandifon its own line. A longand/or, operator or conditional expression goes in parentheses, one operand on each line, operator first. - Spaces. Keyword arguments and defaults are written
name = value. - Blank lines. 2 around top-level definitions and methods, and after the imports. Runs of 6 or more assignments split into groups of 3 to 5. A long
for,whileorifgets a blank line after it. - Classes. Dunders, classmethods and properties move up, in that order, constructors first. Overloads and property setters move with their group.
- Imports. Standard library, third party, local. Shortest first in each group.
All the rules
Line breaks
- If a call, definition, import or literal does not fit on one line, each item goes on its own line.
- If a comprehension does not fit on one line, the element and each
forandifclause go on their own lines. - If an expression with
and,oror a binary operator (+ - * / // % ** @ | & ^ << >>) does not fit on one line, it goes in parentheses. Each operand goes on its own line, with the operator first. - The split occurs at the operator that binds most loosely. For example,
a + b * csplits at+only. An expression with a comparison ornotat the top does not split at its arithmetic operators. - If a conditional expression (
a if condition else b) does not fit on one line, it goes in parentheses. The value, theifpart and theelsepart each go on their own line. - Existing trailing commas do not keep a bracket split. If the items fit on one line, they go on one line.
Blank lines
- Top-level functions and classes have 2 blank lines around them. The imports at the top of a file also have 2 blank lines after them.
- Methods and nested classes have 2 blank lines between them. The first method has 1 blank line above it.
- A class docstring has no blank line after it, before the class attributes.
- In a class of 5 lines or fewer, blank lines not counted, there are no blank lines between the attributes.
- Single-line assignments in a row form a run. A run of up to 5 stays together. A run of 6 or more splits into groups of 3 to 5, as even as possible: 6 is 3 + 3, 7 is 4 + 3. A run of 2 or more has a blank line after it.
- In a function body, a blank line also goes before a comment on its own line, between two statements that each span more than one line, and after an
ifthat ends inreturn,raise,continueorbreak. - No blank line is added above a
return, and tailor does not remove blank lines that you add in a function body. - A
for,whileorifwith more than 2 lines in its body has a blank line after it. - There are no blank lines between the
caseblocks of amatch.
Classes
- Dunder methods, classmethods and properties move up, after the attributes and before the other methods, in that order. Other methods keep their order.
- The constructors come first among the dunders. The other dunders, the classmethods and the properties each follow shortest body first.
- All definitions of one name move together, in their written order:
@overloadstubs and the implementation, or a property and its setter. - Dunders with a body of 1 or 2 lines have 1 blank line between them.
- A comment at the indentation of a method moves with the method below it. A deeper comment stays at the end of the method above it.
- The closing quotes of a class docstring go on its last line of text, if that line then fits.
Imports and type statements
- The imports at the top of a file are in 3 groups: standard library, third party, local. In each group, the shortest import comes first. Imports that span more than one line come last. The names in a
fromimport are also sorted by length. import a, bsplits into one import for each module.- Local imports are relative imports, and the packages and modules in the project root or its
src/folder. - If comments are between the imports, or two imports bind the same name, the import order stays as written.
- Module-level
typestatements move up to after the imports and thelogger = ...line, unless a statement above them uses their name. - An alias value can use a module-level function, class or constant as a value: in a call, or in the metadata of
Annotated. That definition moves up above the aliases, with its decorators and the comments above it. The definitions that it uses when Python defines it move too. Function bodies do not count. Annotations count only if tailor finds no target Python for the project. - If such a definition cannot move, the aliases keep their place, and tailor shows a warning with the alias and the name. For example, a definition that uses an alias in a default value cannot move, and a name that the file binds twice cannot move.
SQL
A string whose first line is """--sql holds SQL. tailor lays out SQLite SQL with syntaqlite and other dialects with sqruff:
row = db.fetch_one(
"""--sql
SELECT
@assists
- (
SELECT COUNT(*)
FROM jobs
WHERE
json_extract(data, '$.user_id') = @user
AND status NOT IN ('FAILED', 'CANCELLED')
) AS assists_left
""",
bindings = {"user": user, "assists": assists},
)
A clause that fits stays on one line. Keywords are upper case, and the SQL starts at the column of the opening quotes. SQLite SQL that ends with ; keeps one ; after each statement. tailor adds no ; to other SQL, because that SQL can be part of a longer query. tailor compares the SQL tokens before and after: if more than whitespace or case would change, or the SQL does not parse, the string stays as written.
tailor finds the dialect of each file in this order:
- The
sql-dialectoption. - The
dialectin the nearest.sqruffor.sqlflufffile. - The database driver that the file imports, for example
import duckdb. - The database driver that the project depends on in
pyproject.toml. - SQLite. If the project depends on drivers of two databases, tailor also shows a warning.
SQL details
- Driver placeholders stay as written:
?,?1,$1,:name,@name,%sand%(name)s. SQLite has no%sor%(name)s, so SQLite SQL with them stays as written. - The SQL stays as written if the string has a backslash, a dollar-quoted value (
$$...$$), a quoted value or name across a line break, or two quoted values separated by a line break. - A first line with more text after
--sqlis a SQL comment, and the string stays as written. - Outside SQLite and MySQL, the case of unquoted names can also change.
- sqruff lays out all the SQL of a file at one width: the width of the string that is indented the most.
- The dialect names are sqruff's:
ansi,athena,bigquery,clickhouse,databricks,db2,duckdb,exasol,greenplum,hive,materialize,mysql,oracle,postgres,redshift,snowflake,sparksql,sqlite,starrocks,teradata,trinoandtsql.
Notes
- Do not run
ruff formatafter tailor. It reverses the keyword spacing, the comprehension layout and the blank lines. - Turn off ruff's import sorting (
I), and the pycodestyle rules for=spacing (E251, E252) and blank lines (E30x). The otherruff checkrules can stay. tailor uses your ruff formatter settings, such asquote-style. - tailor compares the syntax tree of ruff's output with its own result. If anything other than the order of the imports, the class members, the
typestatements and the definitions that the aliases use changed, it writes nothing and shows an error. - tailor moves class members without a check of what they use when the class is created. A decorator or default value that uses another member of the class can fail after the move.
- A definition that moves above the aliases runs earlier when Python loads the module. If its decorator or its value has a side effect, that side effect also occurs earlier. An example is the registration order of routes.
- tailor also changes
# fmt: offand# fmt: skipregions. A file that containsǂǂis not formatted: tailor uses these characters while ruff runs.
License
MIT
Metadata
Release files for pytailor 1.0.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pytailor-1.0.2.tar.gz | 40.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytailor-1.0.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 88.8 kB
Release files / pytailor-1.0.2.tar.gz
| Download URL | pytailor-1.0.2.tar.gz |
|---|---|
| Size | 40.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
01c3cef070e0d7f9f15b47f54b9b59ccc1ec5c06a78cd9beed393f2c5f5598ec
|
|
BLAKE2b-256 checksum How to use checksums |
2950d2fab90612ddb45dd527dbf5a1cdd868114f3bb3ebab5d2439d5c7a16e6e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / pytailor-1.0.2-py3-none-any.whl
| Download URL | pytailor-1.0.2-py3-none-any.whl |
|---|---|
| Size | 48.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
83952a8ea8bde930ce3d8aec2af85359df5169e4faac4eb800f5e4c153c64407
|
|
BLAKE2b-256 checksum How to use checksums |
aab1cc47f805a297cd981cfc8a2129844572462f36a22eeafadf04a7911a6f82
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|