Recently, I was giving a lecture about Java’s “common” methods (i.e., all of the methods of Object), and I had epiphany about how Java only has toString()
while Python has str()
and repr()
. So, it got me thinking: why did Python go with two different methods?
Table of Contents
- Converting Objects to Strings
- Introducing str() and repr()
- Python Might Not Have Had Design by Contract in Mind
- Until Next Time
Converting Objects to Strings
A common desire in many programming languages is to convert the data we’re storing to text that is readable by a variety of audiences. For example, in Java, there is a method called toString()
, which all objects inherit. By default, it prints out an address-like value. When overridden, it can print out anything you want.
One thing that always sort of bothered me about methods like toString()
is that there’s only ever a single implementation. In other words, languages never really give the user an opportunity to provide different string values depending on the audience.
I think I fully came to terms with this issue when I started learning (and consequently teaching) design by contract. Under design by contract, we think about software in terms of two key audiences: clients and implementers. At any given time, we’re both of those; clients of the methods we use and implementers of the methods we write.
Therefore, shouldn’t there be two separate toString()
methods? One clients can use, and one implementers can use. After all, clients really only care about the things they can see, not the internal details. So, why have a toString()
method that just dumps of bunch of logging information? On the flip side, a client facing toString()
is almost useless to an implementer because it doesn’t tell them about internal values.
Hopefully, by this point, you get where I’m going with this. Python has two different toString()
methods: str()
and repr()
.
Introducing str() and repr()
There are loads of articles on the internet that will describe the two different string methods in Python, so I won’t rehash a lot of what others have said. That said, the general difference between str()
and repr()
is that str()
is meant to be human-readable while repr()
is meant to be loaded with information.
As a result, going back to the concept of design by contract, str()
is the client-sided method while repr()
is the implementer-sided method. This becomes much more clear when you see where each method is typically used. For example, when you print an object in Python, the print()
method makes use of str()
. Because printing is generally meant to provide information to the user and not for debugging, this makes sense.
Another example that comes to mind is string formatting. By default, when you build an f-string, it calls the str()
method. Presumably, f-strings must be targeted at the client. This becomes clear because debugging tools like logging don’t want you to use f-strings at all. Though, that could be totally unrelated.
Python Might Not Have Had Design by Contract in Mind
While design by contract helps us reason about the difference between str()
and repr()
, I think there’s a more interesting reason for the existence of the two methods. Specifically, you can print out their “help” description to see what I mean:
help(repr) Help on built-in function repr in module builtins: repr(obj, /) Return the canonical string representation of the object. For many object types, including most builtins, eval(repr(obj)) == obj.
Okay, you don’t really get a nice message like this for str()
since it’s a class. Regardless, I think there’s an interesting secret here: repr()
was included with the intent of being able to generate Python code. In other words, according to the docs, repr()
should return a string that can be evaluated to create a copy of the original object.
One of the best examples of this is Python dataclasses, which let you create objects with even less boilerplate (i.e., no constructor, no getters, no setters, etc.). By default, a dataclass prints its own constructor out with all of the needed fields filled in:
from dataclasses import dataclass @dataclass class Fireball: temperature: int radius: int attack = Fireball(212, 5) print(attack) # prints Fireball(temperature=212, radius=5)
And if you play around, you’ll see that both str()
and repr()
print the same thing:
repr(attack) # returns 'Fireball(temperature=212, radius=5)' str(attack) # returns 'Fireball(temperature=212, radius=5)'
This is perhaps unsurprising because str()
calls repr()
if str()
is not implemented. Regardless, I found this feature so interesting that I tried to repeat it in my own libraries. For example, in my SnakeMD project, you can actually print an entire markdown document as its Python code, even if you don’t build the document using objects directly. The result is a reproducible document that serves almost like its own programming language (e.g., it reminds me of HTML). Take a look:
import snakemd doc = snakemd.new_doc() doc.add_heading("My Cool Document") doc.add_paragraph("Check out how cool this is!") str(doc) # returns '# My Cool Document\n\nCheck out how cool this is!' repr(doc) # returns "Document(elements=[Heading(text=[Inline(text='My Cool Document', image=None, link=None, bold=False, italics=False, strikethrough=False, code=False)], level=1), Paragraph(content=[Inline(text='Check out how cool this is!', image=None, link=None, bold=False, italics=False, strikethrough=False, code=False)])])"
Though, at the time of writing, I’m noticing that this feature is still in beta. Maybe I will release it now.
Until Next Time
With all that said, hopefully you have a new perspective on strings in programming languages. While many programming languages have string features, the fact that Python split theirs out into two meaningful methods gives us a lot of power as developers.
As usual, if you liked this, there’s a whole lot more like it throughout the site. At the time of writing, this is the 577th article published on this site. Surely, at least one of the following is interesting to you:
- Abusing Python’s Operator Overloading Feature
- Dump Java: Life Is Just Better in Python
- Making a Discord Bot Roll a Die in Python
And if you enjoy reading stuff that I didn’t write, check some of these out (#ad):
- Effective Python: 90 Specific Ways to Write Better Python
- Python Tricks: A Buffet of Awesome Python Features
- Python Programming: An Introduction to Computer Science
Finally, you can take your support even further beyond by heading to my list of ways to grow the site. See you next time!
Recent Posts
Recently, I was thinking about the old Pavlov's dog story and how we hardly treat our students any different. While reflecting on this idea, I decided to write the whole thing up for others to read....
In the world of programming languages, expressions are an interesting concept that folks tend to implicitly understand but might not be able to define. As a result, I figured I'd take a crack at...