Padding is not alignment¶
Level: 201 · for Python programmers
One line: ljust, rjust, center, zfill and expandtabs all do one job — make a column line up — and they share one failure: they count code points while a terminal lays out cells, so 'Reid'.ljust(10) and '日本'.ljust(10) are both ten characters long and only one of them is ten columns wide.
len("Reid".ljust(10)) # 10
len("日本".ljust(10)) # 10 -- and twelve columns on screen
"-42".zfill(5) # '-0042' -- not '00-42'
"01\t012".expandtabs(4) # '01 012' -- two spaces, because a tab is arithmetic
Four of them have a format specification that duplicates them — '{:<10}', '{:>10}', '{:^10}', '{:05d}' — and expandtabs has none, because the mini-language has no tab-stop slot. The usual question about a pair like that is which one should I use, and the usual answer, "the newer one", is wrong three times over here. One pair disagrees with itself outright. One agrees only as long as the value is already the right type. And on bytes the newer half does not exist at all.
The deeper problem is one both halves have. Padding is arithmetic on len(), and len() counts code points (counting characters). A terminal, a fixed-width font and a printed report all lay text out in cells, and the number of cells a string occupies is a different number — sometimes larger, sometimes smaller, and for some strings not determined by the string at all. Every method on this page pads to the first number and every reader looks at the second, which is why a column of names from more than one country comes out ragged no matter which spelling you picked.
The program below never asks the terminal anything. Cell counts come from unicodedata.east_asian_width ↗, which is a property of the character database rather than of the machine, so the numbers are the same everywhere — and the recorded output is byte-identical on CPython 3.11, 3.12, 3.13 and 3.14 despite four different revisions of the Unicode database underneath.
Verified output of padding_is_not_alignment_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. FIVE PAIRS: A METHOD, AND THE SPEC THAT DUPLICATES IT
the value is 'ab', the field is 6 wide, so four characters to place
method spec result agree?
---------------------------------------------------------------
'ab'.ljust(6) format('ab', '<6') 'ab ' yes
'ab'.rjust(6) format('ab', '>6') ' ab' yes
'ab'.center(6) format('ab', '^6') ' ab ' yes
'ab'.ljust(6, '.') format('ab', '.<6') 'ab....' yes
'42'.zfill(6) format(42, '06d') '000042' yes
Five names for one job. The methods came first; the mini-language
arrived with str.format and absorbed them. Nothing was removed, so
both spellings are still here and both are still correct. On these
inputs they agree exactly -- sections 4 and 5 are where they stop.
expandtabs is the one method with no spec at all: the mini-language
has no tab-stop slot, so there is nothing for it to have absorbed.
2. NOT ONE OF THE FIVE TRUNCATES
'Bartholomew' is 11 characters, and every field below asks for 6:
call result length
-------------------------------------------------------
'Bartholomew'.ljust(6) 'Bartholomew' 11
'Bartholomew'.rjust(6) 'Bartholomew' 11
'Bartholomew'.center(6) 'Bartholomew' 11
'Bartholomew'.zfill(6) 'Bartholomew' 11
format('Bartholomew', '<6') 'Bartholomew' 11
ljust(6) does not even copy -- it returns the same object: True
So a 'fixed-width' column built from these is not fixed width. It is
a MINIMUM width, and one long value makes the whole table ragged
without raising anything. Two things in the standard library do cut:
format('Bartholomew', '<6.6') 'Bartho' 6 wide, always
struct.pack('6s', b'Bartholomew') b'Bartho' 6 wide, always
struct.pack('6s', b'ab') b'ab\x00\x00\x00\x00' 6 wide, always
3. THE PADDING IS COUNTED IN CODE POINTS; A TERMINAL LAYS OUT CELLS
len() is the number every method on this page pads to. The two right
columns are what the character database says the string occupies.
string len() cells cells if 'A' is wide
--------------------------------------------------------------------
'\u0141\xf3d\u017a' 4 4 6 Latin-2, composed
'\u0141o\u0301dz\u0301' 6 4 5 the same word, decomposed
'\u65e5\u672c' 2 4 4 CJK
'\uff21\uff22' 2 4 4 fullwidth Latin
'\U0001f44d' 1 2 2 emoji
'\U0001f469\u200d\U0001f4bb' 3 4 4 emoji, ZWJ sequence
'Reid' 4 4 4 ASCII, for scale
ljust(10), then a bar, so the ragged edge is visible:
[Łódź ] padding added 6, cells 10
[Łódź ] padding added 4, cells 8
[日本 ] padding added 8, cells 12
[AB ] padding added 8, cells 12
[👍 ] padding added 9, cells 11
[👩💻 ] padding added 7, cells 11
[Reid ] padding added 6, cells 10
The same list padded by CELLS instead of by code points:
[Łódź ]
[Łódź ]
[日本 ]
[AB ]
[👍 ]
[👩💻 ]
[Reid ]
Nothing in the standard library computes that second column.
unicodedata.east_asian_width is the raw material; the model at the
top of this file -- W and F are two cells, combining marks are zero
-- is a convention, and the convention has two holes of its own:
Hole 1: 'A' means AMBIGUOUS, and two of the four letters in Łódź are
in it.
'\u0141' LATIN CAPITAL LETTER L WITH STROKE EAW=A
'\xf3' LATIN SMALL LETTER O WITH ACUTE EAW=A
One cell in a Western terminal, two in a legacy East Asian one. The
string does not carry the answer; the terminal's configuration does.
Hole 2: East_Asian_Width predates emoji sequences.
'\U0001f469' WOMAN EAW=W
'\u200d' ZERO WIDTH JOINER EAW=N
'\U0001f4bb' PERSONAL COMPUTER EAW=W
The model scores that at 4 cells -- two Wide characters, joiner
skipped -- but a terminal that supports the sequence draws ONE glyph,
two cells wide. So the column-aware padding above is wrong too, just
less often. There is no correct answer inside the standard library.
4. center AND ^ DISAGREE ABOUT WHERE THE ODD SPACE GOES
'ab' -- an even length -- in fields of every width from 3 to 9
width 'ab'.center(w) format('ab', '^w')
---------------------------------------------------
3 ' ab' 'ab ' <- DIFFER
4 ' ab ' ' ab '
5 ' ab ' ' ab ' <- DIFFER
6 ' ab ' ' ab '
7 ' ab ' ' ab ' <- DIFFER
8 ' ab ' ' ab '
9 ' ab ' ' ab ' <- DIFFER
center's tiebreak reads the parity of BOTH numbers. CPython computes
left = margin // 2 + (margin & width & 1), so the spare space goes
LEFT when margin and width are both odd. The format spec has no such
rule: '^' always puts pad // 2 on the left and the remainder on the
right. Two spellings of 'centre this', one apart, in exactly the case
where centring is genuinely ambiguous.
5. zfill IS SIGN-AWARE, AND THE SPEC THAT LOOKS LIKE IT IS NOT
input zfill(5) what it did
---------------------------------------------------------
'-42' '-0042' sign kept in front, zeros behind it
'+4' '+0004' '+' is a sign too
'42' '00042' no sign, so the zeros go all the way
'-' '-0000' a lone sign is still a sign
'--42' '-0-42' only the FIRST character is a sign
'a-42' '0a-42' not at position 0, so not a sign
'-4.2' '-04.2' it never looks at what follows
'' '00000' an empty string is five zeros
Four ways to ask for 'five wide, zero padded', and three answers:
'-42'.zfill(5) '-0042' the sign is found and kept in place
format(-42, '05d') '-0042' the int agrees with zfill
format('-42', '0>5') '00-42' fill='0', align='>': padding, no sign
format('-42', '05') '-4200' and this one is a different bug
The last line is the trap. On an int, a '0' before the width is a
flag meaning 'pad after the sign'. On a str there is no sign to pad
after, so the '0' degrades to a plain fill character -- and the
default alignment for a str is LEFT, which puts the zeros on the
wrong end of the number. zfill is '{:05d}' for a value that is
already text, and it is that for no other spelling.
6. expandtabs IS COLUMN ARITHMETIC, NOT REPLACEMENT
'01\t012\t0123'.expandtabs(4)
-> '01 012 0123'
Two spaces, then one. A tab is not N spaces; it is 'advance to the
next multiple of N', so what one expands to depends on everything
in front of it on the line:
piece column before next stop spaces written
---------------------------------------------------
'01' 2 4 2
'012' 7 8 1
'0123' 12 - -
tabsize 'a\tb'.expandtabs(n)
8 'a b'
4 'a b'
1 'a b'
0 'ab'
-1 'ab'
Zero and negative delete the tab rather than raising.
It counts characters, so it misses a wide column for exactly the
reason ljust does -- the next tab stop after 'X' should be cell 4:
'ab\tX' -> 'ab X' X lands at cell 4
'\u65e5\u672c\tX' -> '\u65e5\u672c X' X lands at cell 6 <- not 4
'\U0001f44d\tX' -> '\U0001f44d X' X lands at cell 5 <- not 4
And what resets the column back to zero is a shorter list than you
would guess from splitlines:
separator expandtabs resets? splitlines() splits?
--------------------------------------------------------------
LINE FEED yes yes
CARRIAGE RETURN yes yes
LINE TABULATION no yes
FORM FEED no yes
FILE SEPARATOR no yes
NEXT LINE no yes
LINE SEPARATOR no yes
PARAGRAPH SEPARATOR no yes
Two against eight. Every separator here begins a new line as far as
splitlines is concerned; only LF and CR begin one as far as the tab
stops are concerned.
7. ON bytes THE METHODS SURVIVE AND THE BRACES DO NOT
b'ab'.ljust(5, b'.') b'ab...'
b'-42'.zfill(5) b'-0042'
b'01\t012'.expandtabs(4) b'01 012'
b'%-5s|' % b'ab' b'ab |'
format(b'ab', '<5') TypeError
str.format and f-strings do not exist on bytes, so for binary output
the padding methods and % are the whole toolbox. That is the honest
answer to 'why are there two ways to do this': one of the two still
works on the type the other one never learned.
What the run shows¶
Five pairs, and on ordinary input each pair is one call written twice. 'ab'.ljust(6) and format('ab', '<6') produce identical strings, as do the rjust/>, center/^, ljust(6, '.')/'.<6' and zfill/'06d' pairs. That is the expected result and it is worth establishing, because the two places they diverge are easy to write off as typos otherwise.
Not one of the five truncates. 'Bartholomew'.ljust(6) is 'Bartholomew' — eleven characters out of a field declared six wide — and so are rjust, center, zfill and format('Bartholomew', '<6'). ljust does not even copy: it returns the very same object. So the width argument is a minimum, never a maximum, and a table built out of these methods stays neat exactly until one value is too long, at which point every column after it shifts and nothing raises. The standard library does contain field widths that hold: .precision on a string truncates, so format('Bartholomew', '<6.6') is 'Bartho' and '{:<6.6}' is the spelling of a genuinely fixed-width text column; and struct ↗ '6s' truncates the long value and NUL-pads the short one, which is what a fixed-width record format has always meant.
The padding is counted in code points and the reader sees cells. '日本' is two code points and four cells, so ljust(10) adds eight spaces and produces twelve columns. '👍' is one code point and two cells, so it produces eleven. Decomposed 'Łódź' — the same word with its accents as separate combining marks — is six code points and four cells, so ljust(10) adds only four spaces and produces eight. Three different wrong answers from one method, on three strings that are all "length 10" afterwards.
And the cell count is a convention, not an API. Nothing in the standard library computes it. east_asian_width gives the raw material and the model on top of it — W and F are two cells, combining marks are zero — is the one every terminal approximates, with two holes the run makes visible. Two of the four letters in Łódź are East_Asian_Width=A, Ambiguous: one cell in a Western terminal and two in a legacy East Asian one, a fact about the reader's configuration that the string does not carry. And East_Asian_Width predates emoji sequences, so the model scores 👩💻 at four cells — two Wide characters, joiner skipped — where a terminal that supports the sequence draws one glyph two cells wide. The column-aware padding in the run is less wrong than ljust, not right.
center and ^ disagree, by one space, in the case where centring is ambiguous. 'ab'.center(5) is ' ab ' and format('ab', '^5') is ' ab '. CPython's center computes left = margin // 2 + (margin & width & 1), so the spare space goes on the left when the margin and the width are both odd; the format spec has no such rule and always puts pad // 2 on the left. That is a real difference between two spellings of one idea, it shows up whenever an even-length value meets an odd-width field, and neither behaviour is documented — which means neither is a promise.
zfill finds the sign; the spec that looks like it does not. '-42'.zfill(5) is '-0042', and '+4'.zfill(5) is '+0004': the leading + or - is held in place and the zeros go behind it. The rule is narrow and positional — only the first character counts, so '--42'.zfill(5) is '-0-42' and 'a-42'.zfill(5) is '0a-42' — and zfill never looks at the rest, so it will happily zero-pad '-4.2'. Four ways to ask for "five wide, zero padded" give three answers:
| the call | result | |
|---|---|---|
'-42'.zfill(5) |
'-0042' |
the sign is found and kept in front |
format(-42, '05d') |
'-0042' |
the int agrees with zfill |
format('-42', '0>5') |
'00-42' |
fill='0', align='>' — padding, no sign logic |
format('-42', '05') |
'-4200' |
a different bug |
The last row is the one to remember. On an int, a 0 before the width is a flag meaning pad after the sign. On a str there is no sign to pad after, so the 0 degrades into a plain fill character, and the default alignment for a str is left — which puts the zeros on the wrong end of the number and produces something that still looks vaguely numeric. So zfill is '{:05d}' only for a value that is already text; write '{:05}' for a string and you get neither.
expandtabs is column arithmetic, not replacement. '01\t012\t0123'.expandtabs(4) is '01 012 0123' — two spaces, then one — because a tab does not mean N spaces, it means advance to the next multiple of N. What a given tab expands to therefore depends on everything in front of it on the line, which is why replacing tabs with a fixed run of spaces changes a file's layout and expandtabs does not. tabsize=0 and any negative value delete the tab rather than raising.
And expandtabs counts characters, so it misses a wide column for exactly the reason ljust does. After 'ab\t' at tabsize=4 the next character lands at cell 4, which is the point of a tab stop; after '日本\t' it lands at cell 6 and after '👍\t' at cell 5. The shared cause is why these five methods are one page and not three.
What resets the tab column is a much shorter list than what ends a line. Only LF and CR set the column back to zero. VT, FF, FS, NEL, LINE SEPARATOR and PARAGRAPH SEPARATOR do not — and all eight of them split a string under splitlines() (what ends a line). Two against eight, in one standard library, over the question of what a new line is.
On bytes the methods survive and the braces do not. b'ab'.ljust(5, b'.'), b'-42'.zfill(5) and b'01\t012'.expandtabs(4) all work; format(b'ab', '<5') is a TypeError, because str.format and f-strings do not exist on bytes. The % operator does (PEP 461 ↗), so b'%-5s|' % b'ab' is b'ab |'. That is the honest answer to why are there two ways to do this: the older way still works on a type the newer one never learned.
So which one do I use¶
For a table, use the format mini-language, and pair the width with a precision — '{:<20.20}' is the fixed-width text column that ljust(20) only pretends to be. Use zfill when the value is already a string with a sign in it and '{:05d}' when it is a number. Use expandtabs when you are rendering a file that contains tabs, and never as a way to indent.
For anything with non-ASCII in it, neither one is enough, and the fix is not a better method — it is to stop laying out text with padding. Emit a delimited format and let something else align it (column -t, a spreadsheet, a Markdown table renderer), or measure cells yourself with east_asian_width and accept that the answer is a model. A third-party library — wcwidth for the cell count, rich or tabulate for the whole job — is the right call in production and is deliberately outside this library's stdlib-only rule.
The same grammar, different slot order¶
Rust's format! uses the same grammar for this: {:<10}, {:>10}, {:^10} and {:010} mean what they mean here, and its {:010} of -42 is -000000042 for the same sign-aware reason zfill is. The sibling library owns that page — the format language ↗ — and the ragged-column half of this one has a Rust counterpart in four lengths ↗.
C# is the useful contrast, because it is the same grammar with the slots in a different place. Console.WriteLine("{0,-20} {1,5:N1}", name, value) puts the alignment inside the field and before the colon — ,-20 is "left-aligned, 20 wide" — where Python puts it after the colon, inside the format spec: '{0:<20} {1:5.1f}'. Same three decisions (which argument, how wide, which way up), split between the two halves of the braces differently. It is worth knowing before you read a .NET format string as a Python one and misparse the comma as a thousands separator, which is exactly what a comma means in Python's half.
If you are coming from ABAP¶
This is the sharpest contrast on the page, because in ABAP the padding is in the type rather than in a call. DATA lv_name TYPE c LENGTH 20 is genuinely twenty characters wide: assign a shorter value and it is blank-padded on the right for you, assign a longer one and it is truncated, silently, at the assignment. There is no ljust because there is nothing to left-justify — the field was already the right size, and CONDENSE exists to undo the padding rather than to apply it. Python inverts every part of that. A str has no declared width, padding happens only where you write it, and the width you write is a floor rather than a ceiling — so the ABAP failure mode (your data was cut and you were not told) becomes the Python failure mode (your column was not cut and your table is now crooked). Coming this way, the habit to carry over is that a fixed-width output format is a contract, and to write the truncation yourself — '{:<20.20}' — rather than hoping the data fits. Coming the other way, the habit to drop is trusting the type: nothing in Python will hold a column to a width unless you say so twice. String templates and WRITE ... AT have their own rules again; check the alignment behaviour on your own system rather than assuming it matches either of these. (Not machine-checked — CI cannot run ABAP.)
Try it¶
- Take the
cells()function from the program and write theljustthat uses it. Then feed it the ZWJ sequence and decide what your function should do — there is no right answer, and choosing one is the lesson. 'ab'.center(w)andformat('ab', '^' + str(w))disagree for oddw. Find the shortest value/width pair where they disagree, then predict fromleft = margin // 2 + (margin & width & 1)— without running it — whether'abc'.center(8)is one of them.- Build a two-column report with
'{:<20}'and feed it a name longer than twenty characters. Now do it with'{:<20.20}'. Which failure would you rather explain to the person whose name it is? '-4.2'.zfill(8)pads a decimal number. Work out whatzfillwould have to know to refuse, and why it does not know it — then check whatformat(-4.2, '08.1f')does instead.- Write a file with tabs in it, expand them at
tabsize=8and attabsize=4, and diff the results. Then put a\vin the middle of a line and explain, from section 6, why the second half of that line moved. b'%-5s|' % b'ab'pads abytes. Find out whatb'%-5s|' % 'ab'does, and why the answer is different from what the same mistake does onstr.
See also¶
- The format mini-language — the nine slots these five specs are three of, and why
%is a separate language - Counting characters — the four answers to "how long is this string", and why
len()is never the one a reader sees - What ends a line — the eight boundaries
splitlines()knows, against the two that reset a tab column stripis a set, not a prefix — the other page about astrmethod that silently does not do what its name says- Normalization — where the composed and decomposed
Łódźin section 3 come from - What to write next — this page closes rows 10, 14 and 17 of the backlog
- The crosswalk — which idea lives in which library
str.ljustand friends ↗ — the reference entries, including the sentence that says the original is returned unchangedunicodedata.east_asian_width↗ — the only cell-width data in the standard library- UAX #11: East Asian Width ↗ — what
A(Ambiguous) actually means, and why it is a property of the context rather than of the character - The format language ↗ — the same grammar in Rust