repr is not str¶
Level: 201 · for Python programmers
One line: str.isprintable() has nothing to do with ink — it asks whether repr() would escape the character — which is why a space is printable, a tab is not, and string.printable is not printable.
The word is a trap, and the docs know it: string.printable carries a note saying that string.printable.isprintable() returns False by design. Two constants one import apart, spelled the same, meaning different things. Reading str.isprintable() as "will this show up on paper" makes that note look like a bug; reading it as what it actually says — "is this character suitable for repr() to use in its output" — makes it obvious.
So this page is really about the two functions underneath. str(x) and repr(x) are two different questions asked of the same object: show this to a person and show this to a Python programmer. Almost every confusing string in Python is one of them arriving where you expected the other — a tab you cannot see, a b'…' that turned into text, a list that printed escapes when the string inside it did not.
Verified output of repr_is_not_str_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. TWO FUNCTIONS, TWO QUESTIONS
str(x) -- 'show this to a person' -> characters, no quotes
repr(x) -- 'show this to a Python programmer'
ascii(x) is repr(x) with every non-ASCII character escaped.
object repr(x) ascii(x)
--------------------------------------------------------------
'a' 'a' 'a'
'é' 'é' '\xe9'
'😸' '😸' '\U0001f638'
'\t' '\t' '\t'
b'Zoot!' b'Zoot!' b'Zoot!'
Fraction(1, 3) Fraction(1, 3) Fraction(1, 3)
The bytes row is the one to stare at: repr and ascii agree,
and str() -- which is not in this table for a reason -- gives
the same thing again. Section 5.
The cat row does not line up, and that is not a bug in this
table: the columns were padded with ljust, which counts code
points, and that glyph is one code point two columns wide.
2. WHAT 'PRINTABLE' ASKS, CHECKED OVER ALL 1,114,112 CODE POINTS
The docs rule: printable = General_Category in L, M, N, P or S,
plus the ASCII space U+0020. Everything else (Z, C) is not.
code points where isprintable() disagrees with that rule: 0
non-printable code points that are NOT in Z or C: 0
separator characters that ARE printable: U+0020 SPACE
Three families that are non-printable and never change size:
Cc control 65
Cs surrogate 2,048
Co private use 137,468
Cf (format) and Cn (unassigned) are left uncounted on purpose:
both move with the Unicode version, and two Pythons on one
machine are routinely built against different editions of the
table. Print unicodedata.unidata_version to see yours.
3. SO A SPACE IS PRINTABLE AND A TAB IS NOT
string isprintable() isspace()
------------------------------------------------------
'' True False
' ' True True
'\t' False True
'\n' False True
'\u00a0' NO-BREAK SPACE False True
'\u3000' IDEOGRAPHIC SPACE False True
Printable and whitespace are not opposites. U+0020 is both --
it is the single exception written into the rule -- and every
other space character in Unicode is whitespace and not printable.
The empty string is printable for the same vacuous reason it is
not isspace(): no character in it is non-printable.
4. string.printable IS NOT PRINTABLE
string.digits 10 characters, 0 not printable
string.ascii_letters 52 characters, 0 not printable
string.punctuation 32 characters, 0 not printable
string.whitespace 6 characters, 5 not printable
string.printable 100 characters, 5 not printable
the five: '\t' '\n' '\r' '\x0b' '\x0c'
string.printable.isprintable() -> False
Two different words, one spelling. string.printable is the
POSIX sense -- 'characters a terminal can handle' -- and it
includes the whitespace that moves the cursor. str.isprintable()
is the repr() sense, and repr() escapes exactly those five.
5. str(bytes) IS THE QUIET ONE
b b'Zoot!' len 5
str(b) "b'Zoot!'" len 8 <- the repr, as text
str(b, 'utf-8') 'Zoot!' len 5 <- a decode
b.decode() 'Zoot!' len 5
Same function, two signatures, and the one with no encoding
silently gives you a string with 'b' and two quotes in it.
sys.flags.bytes_warning on this run: 0
Run the file again as `python3 -b` and that becomes 1, and
str(b) raises a BytesWarning. `-bb` makes it an error.
6. !s, !r AND !a IN A FORMAT STRING
f'{one_third}' 1/3
f'{one_third!s}' 1/3
f'{one_third!r}' Fraction(1, 3)
f'{one_third = }' one_third = Fraction(1, 3)
f'{text!a}' '\xa1ko\u010dka \U0001f638!'
The default conversion is str(). The debug specifier '=' silently
switches it to repr(), which is the whole point of it: you asked
to see the value as a programmer, not as a reader.
7. A CONTAINER ALWAYS USES repr() ON ITS ELEMENTS
print(value) -> a b
print([value]) -> ['a\tb']
One tab, printed two ways in the same program. The first line
is str(): the tab is in the output and you can only see it by
the gap. The second is repr(), because a list has no way to
show you where one element ends -- so it escapes.
That is why wrapping a value in a list is the cheapest debugging
trick in Python, and why 'printable' had to be defined in terms
of repr() rather than in terms of ink.
8. THE PROMPT CALLS repr() FOR YOU
At >>> the interpreter passes each result to sys.displayhook,
which prints repr() of it. So a function that RETURNS a repr
arrives quoted twice. This section calls the real hook.
you type the prompt shows print(it) shows
--------------------------------------------------------
'café' 'café' café
repr('café') "'café'" 'café'
ascii('café') "'caf\\xe9'" 'caf\xe9'
ascii('abcd') "'abcd'" 'abcd'
len(ascii('café')) 9 9
None (nothing) None
every row is exactly repr(value), and None prints nothing: True
The doubled backslash is the prompt's, not ascii()'s. The string
ascii('café') returns is 9 characters -- two quotes, c, a, f, ONE
backslash, then x, e, 9 -- and the quotes are inside it, which is
why len(ascii('abcd')) is 6 and not 4.
What the run shows¶
Section 2 is the definition, and it holds exactly. Over all 1,114,112 code points, isprintable() agrees with the documented rule — General_Category L, M, N, P or S, plus the ASCII space — with zero disagreements, and every non-printable code point is in Z or C. That is worth checking rather than quoting, because the rule has an exception written into it and an exception is exactly the kind of thing that drifts. The one printable separator in the whole of Unicode is U+0020.
Printable and whitespace are not opposites. U+0020 is both. Every other space character — U+00A0 NO-BREAK SPACE, U+3000 IDEOGRAPHIC SPACE — is whitespace and not printable, because repr() escapes them: they would be invisible in a representation whose whole job is to be unambiguous. That is the answer to "how can you print whitespace?" — you can, and repr() still refuses to, because repr() is not printing it to be read.
string.printable is the POSIX word. It is digits + ascii_letters + punctuation + whitespace, 100 characters, and string.whitespace brings five that repr() escapes: \t \n \r \x0b \x0c. So the constant means "characters a terminal can cope with" and the method means "characters repr() leaves alone", and the five that separate them are precisely the ones that move a cursor without drawing anything. Neither name is wrong; they are answers to different questions that were both called "printable" about twenty years apart.
str(b'Zoot!') is "b'Zoot!'", and nothing warns you. A five-byte object becomes an eight-character string containing a b and two quote marks, because str() with no encoding falls back to repr(). str(b, 'utf-8') is the other signature and does the decode you meant. This is the one to remember from the page: it does not raise, the result is a perfectly good str, and it will travel a long way through your program before anything notices. Python ships a command-line flag for it — python3 -b turns it into a BytesWarning, -bb into an error — which is as close to an apology as a language gets.
The debug specifier silently switches the conversion. f'{x}' uses str(); f'{x = }' uses repr(). That is deliberate and it is the right default — you asked to see the value as a programmer — but it means the same expression prints two different things depending on whether you added an equals sign. Fraction(1, 3) shows it in one line: 1/3 becomes Fraction(1, 3).
And a container always uses repr() on its elements. print(value) gives you a tab you can only detect by the gap; print([value]) gives you ['a\tb']. Nothing about the string changed. This is why wrapping a value in a list is the cheapest debugging trick in Python — and it is the practical reason "printable" had to be defined in terms of repr() in the first place. A representation that renders a tab as a tab is not a representation.
The prompt is repr() too, which is why ascii('café') shows two backslashes at >>>. The interactive interpreter hands every result to sys.displayhook, which prints repr() of it; section 8 calls the real hook and captures what it writes. So a function that returns a repr — ascii() and repr() both do — is quoted twice: "'caf\\xe9'" at the prompt, 'caf\xe9' from print(). len() settles which one is the string: 9 characters with one backslash in them, and len(ascii('abcd')) is 6 because the quotes are part of the value. The one value the hook leaves alone is None, which is why a call to print() at the prompt is not followed by a None line. And that \xe9 is a code point written as an escape, not a byte: String literals has its five spellings.
The Rust view¶
Rust drew the same line and put it in the type system: Display is str() and Debug is repr(), reached as {} and {:?}. Measured side by side, the two languages agree on every sample about what counts as printable, which is the interesting part — nobody coordinated it, and both arrived at "printable means the debug form does not escape it."
character Rust {:?} escape_debug is_control Python isprintable()
---------------------------------------------------------------------------------------------
U+0061 a 'a' a false True
U+00E9 LATIN SMALL E ACUTE 'é' é false True
U+0020 SPACE ' ' (a space) false True
U+0009 TAB '\t' \t true False
U+00A0 NO-BREAK SPACE '\u{a0}' \u{a0} false False
U+3000 IDEOGRAPHIC SPACE '\u{3000}' \u{3000} false False
U+200B ZERO WIDTH SPACE '\u{200b}' \u{200b} false False
Two differences are worth the trip. The first: Rust has no public is_printable. The predicate exists — it is what escape_debug consults — but it is private, and the public method with a related name, char::is_control, answers a much narrower question: only the tab row above is true, while four of the seven are non-printable. If you want Python's isprintable() in Rust you write it out of the categories yourself.
The second is the one that matters in practice. Python's str() falls back to repr() when a type has no __str__, so you can never be certain from the call site which one you got — that is exactly the str(b'Zoot!') bug, one layer down. Rust's two traits are independent, so a type that implements only Debug cannot be printed with {} at all; println!("{}", vec![1]) is a compile error naming the missing trait. Same distinction, and one of the two languages makes you notice it before the program runs.
For a bytes value the Rust counterpart of the b'…' that repr() and the prompt show is escape_ascii() ↗: the same inside on 254 of the 256 byte values, with the two quote characters escaped every time where Python picks a delimiter instead.
The sibling library owns the formatting side of this: the format language ↗ is the {}/{:?} mini-language, which Python's str.format and Rust's format! share by descent. And because section 2 is a table lookup rather than a rule, the table has a version ↗ applies to all of it — the counts of unassigned and format characters move with the Unicode edition your interpreter was built against, which is why this page's program does not print them.
If you are coming from ABAP¶
There is one representation, and it is the value. WRITE renders a field; there is no second function that renders it as source, and no convention that a debugging display should be unambiguous — the Debugger shows you the value and a separate hex view if you ask for it. The practical consequence when you move to Python is that the two habits collide: print(x) is WRITE and will hand you a tab you cannot see, while print([x]) and print(f'{x = }') are the hex-view instinct, available on any object and one character apart from the plain form. Reach for them first when a string "looks right" and compares unequal. For the printability question itself, ABAP's nearest idiom is cl_abap_char_utilities plus a CO test against a character set you wrote by hand, which is string.printable's POSIX sense rather than isprintable()'s — worth knowing on your own system which control characters your code page even admits. (Not machine-checked — CI cannot run ABAP.)
Try it¶
- Run the program again as
python3 -b <path>and thenpython3 -bb <path>. Which section changes, and what does the exception say? Now find where in your own code astr()call could be handed abytes— the answer is usually "wherever something reads a file in binary mode". repr()carries a promise for built-in types:eval(repr(x)) == x. Check it for astr, abytes, alistof both, and afloat— then find the float where it is most interesting, and work out whyrepr(0.1)prints the digits it does.- Write
isprintable()yourself fromunicodedata.category, and diff it against the real one over the whole code space. The program above already does the comparison; do it without looking, and see which exception you forget. f'{ x - 4 = }'keeps every space you typed. Print it and count them. Then decide whether that is a feature — and what it implies about when the debug specifier is parsed.- At a real
>>>prompt, typeascii('café'), thenprint(ascii('café')), thenlen(ascii('café')). Count the backslashes each time, and say which of the three is telling you about the string itself.
Practice¶
Eight one-liners about a word that means two things. Write down each result.
' '.isprintable()'\t'.isprintable()''.isprintable()'\u00a0'.isprintable()string.printable.isprintable()len(str(b'Zoot!'))len(str(b'Zoot!', 'utf-8'))repr(Fraction(1, 3))
Then: the first five are one rule. State it in a sentence that does not contain the word "print". And line 6 is a five-byte object arriving somewhere as eight characters with no exception anywhere — say where in your own code that call could be reached.
Answers
Verified output of repr_is_not_str_kata_py.py — regenerated by tools/run_examples.py, never hand-typed.
expression result note
---------------------------------------------------------------------------------
' '.isprintable() True a space IS printable -- the one exception in the rule
'\t'.isprintable() False a tab is not: repr() escapes it
''.isprintable() True vacuously true -- no character in it is non-printable
'\u00a0'.isprintable() False NO-BREAK SPACE: whitespace, and not printable
string.printable.isprintable() False the constant is not printable, BY DESIGN
len(str(b'Zoot!')) 8 five bytes in, eight characters out
len(str(b'Zoot!', 'utf-8')) 5 the other signature, and the one you meant
repr(Fraction(1, 3)) 'Fraction(1, 3)' what f'{x = }' shows you, and f'{x}' does not
PRINTABLE HAS NOTHING TO DO WITH INK
str.isprintable() asks one question: would repr() escape this?
That is why a space passes and a tab does not, and why every
space character in Unicode EXCEPT U+0020 fails -- a
representation whose job is to be unambiguous cannot render
an invisible character as itself.
string.printable is the OTHER word, the POSIX one: characters
a terminal can cope with, which includes the five that move a
cursor without drawing anything.
the five that separate them: '\t' '\n' '\r' '\x0b' '\x0c'
len(string.printable) 100
Two questions, one spelling, about twenty years apart.
THE QUIET ONE IS LINE 6
b b'Zoot!' len 5
str(b) "b'Zoot!'" len 8 <- the repr, as text
str(b, 'utf-8') 'Zoot!' len 5
b.decode() 'Zoot!' len 5
Nothing raised. A five-byte object became an eight-character
string containing a 'b' and two quote marks, because str() with
no encoding falls back to repr(). It is a perfectly good str and
it will travel a long way before anything notices.
sys.flags.bytes_warning on this run: 0
Run any file with python3 -b and that becomes 1, and str(b)
raises a BytesWarning. -bb makes it an error. Python shipping a
command-line flag for one call is as close to an apology as a
language gets.
THE CHEAPEST DEBUGGING TRICK IN PYTHON
print(value) -> a b
print([value]) -> ['a\tb']
One tab, printed two ways in the same program, and nothing
about the string changed. A container has no way to show you
where one element ends, so it always uses repr() on what is
inside -- which is the practical reason 'printable' had to be
defined in terms of repr() in the first place.
f'{x}' 1/3
f'{x!r}' Fraction(1, 3)
f'{x = }' one_third = Fraction(1, 3)
The debug specifier silently switches the conversion to repr().
Same expression, one equals sign, two different strings.
See also¶
- Is it a letter? — the other eleven
is*predicates, and the empty-string rule that splits the family stris notbytes— the type boundary thatstr(b'…')quietly crosses- Counting characters — why the cat in section 1 does not line up
- What to write next — this page closes three questions; the format mini-language is the next one
- The crosswalk — which idea lives in which library
str.isprintable()in the Python docs ↗ — the definition this page is unpackingstring.printable↗ — and the note that says it is not printable