Skip to content

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."

Measured 2026-09-07 — not verbatim program output; rustc 1.98.0 and Python 3.14.7, put side by side
  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

  1. Run the program again as python3 -b <path> and then python3 -bb <path>. Which section changes, and what does the exception say? Now find where in your own code a str() call could be handed a bytes — the answer is usually "wherever something reads a file in binary mode".
  2. repr() carries a promise for built-in types: eval(repr(x)) == x. Check it for a str, a bytes, a list of both, and a float — then find the float where it is most interesting, and work out why repr(0.1) prints the digits it does.
  3. Write isprintable() yourself from unicodedata.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.
  4. 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.
  5. At a real >>> prompt, type ascii('café'), then print(ascii('café')), then len(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.

  1. ' '.isprintable()
  2. '\t'.isprintable()
  3. ''.isprintable()
  4. '\u00a0'.isprintable()
  5. string.printable.isprintable()
  6. len(str(b'Zoot!'))
  7. len(str(b'Zoot!', 'utf-8'))
  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