Why Do 5 + "5" and "5" + 5 Give Different Errors in Python? • Do You Know The Whole Story?
If __radd__() is not part of your answer, read on…
5 + "5"
and"5" + 5
give a different error message because the first calls theint
class's__add__()
dunder method whereas the second calls thestr
class's__add__()
method
…is not a sufficient answer.
I’ve often given this explanation when teaching. And it’s not wrong. I’ll still use it in introductory courses.
But a serendipitous exploration led me down a different route recently…
Why __add__()
Alone Doesn't Fully Explain This
Let's start by looking at the error messages for the two versions:
In the first example, the integer 5
comes first, before the +
operator. The string "5"
is after the +
. This raises a TypeError
, which tells you the types you used with the +
operator are unsupported.
You can't add the string "5"
to the integer 5
. This probably makes sense.
In the second example, the order of the data types is swapped. The string "5"
is before the +
operator and the integer 5
after. You still get a TypeError
, but the message is different. This error message is specific about concatenation of strings. You can only add a string to another string.
First attempt
The +
operator calls the __add__()
special method of the first operand—the object on the left of the +
operator. Special methods are methods that allow objects to be used with several operators and built-in functions. The special method names start and end with double underscores, leading to their informal name of dunder methods. I'll be writing in more detail about classes in the near future.
In the expressions you tried to evaluate above, the first operand is an integer in one example and a string in the other. This should explain why the error messages are different, right?
Yes, but only in part.
Let's start with the version where the string is the first operand: "5" + 5
This expression is the same as the following:
This is the same error message you get when you try to evaluate "5" + 5
. So, what's the whole fuss, you may be thinking?
Let's try the same with the expression in which the integer comes first: 5 + "5"
But before we can get to the main point of this discussion, you encounter an issue:
There's the 'inconvenience' of a SyntaxError
since the .
after a number is used to denote floats and not to access attributes of the int
class. There are a few workarounds to this. Here's the clearest one:
You no longer need to use the integer literal 5
directly to call the __add__()
special method. Great! That problem is solved.
However, you don't get the same error message as when you evaluate 5 + "5"
. When you call the __add__()
method directly, it returns NotImplemented
. This is a special value and the only instance of the NotImplementedType
!
We'll soon carry on down the path this leads us. But first, you may be curious about the workarounds I mentioned earlier to deal with the issue that you can't write 5.__add__("5")
. You've seen the solution in which you define a variable name first. You can also call __add__()
directly by using the class name:
You need to include both objects as arguments since you're not calling __add__()
as a method on an integer object. The other option is a "cheat" I've only learned about recently. You can add a space after the integer literal and call the method without the need for a variable name:
Note the space between 5
and .
in this version. It doesn't matter which option you choose. They do the same thing.†
† Edit: Thanks to those who pointed out there’s another option: (5).__add__(“5”)
Enter __radd__()
Here's what's missing in this discussion so far. Warning: dense sentence coming up next! When the __add__()
special method returns NotImplemented
, the interpreter falls back to the second operand's reflected addition operator. A lot is happening in that sentence. Let's unpack it:
5 + "5"
first calls the__add__()
special method in theint
class:5 .__add__("5")
This call returns
NotImplemented
The interpreter next looks at the second operand, which is a string in this case. It calls its
__radd__()
special method. This is the addition special method with reflected operands.
Therefore,"5".__radd__(5)
is called
When 5 .__add__("5")
returns NotImplemented
, the interpreter gets its cue to call "5".__radd__(5)
. Note that this is the __radd__()
method for the string class. Let's see what's the output:
The string class doesn't have a __radd__()
method. Strings can only be added to other strings.
Therefore, neither int.__add__()
nor str.__radd__()
can deal with adding integers to strings. At this point, the interpreter raises the TypeError
you got earlier: unsupported operand type(s) for +: 'int' and 'str'.
Let's Try To Multiply Instead
Let's try a similar approach using multiplication instead of addition:
The behaviour is different in this case. Both versions give the same result: the string containing the character "5"
is repeated five times.
Let's drill into both of these expressions, starting with the one where the string is the first (or left) operand.
The dunder method associated with the *
operator is __mul__()
. Therefore, "5" * 5
starts by calling the __mul__()
special method in the string class:
This method, str.__mul__()
, is implemented and returns the string repeated five times.
Let's look at the version with the integer in the first (left) position: 5 * "5"
. In this case, the integer class's __mul__()
method is called:
The __mul__()
method in the integer class returns NotImplemented
when a string is passed as an argument.
The interpreter now falls back to the reflected multiplication method in the right operand's class. That's the string in this case:
The reflected version of the multiplication special method is similar to the standard version:
Therefore, 5 * "5"
returns the expected value not because the __mul__()
method in the integer class deals with this scenario. Instead, it's the string's reflected multiplication special method, __rmul__()
, that takes care of this case.
A Final Example With A User-Defined Class
Let's see whether this approach can also work with user-defined classes. You can create a simple class called SubstackArticle
with only one data attribute, .number_of_words
. I'm keeping this class very simple for demonstration purposes:
You define the class and create an instance of this class called my_article
. It doesn't matter in which order you try to multiply my_article
and an integer. Neither option works. The code raises a TypeError
in both scenarios.
Next, you can define this class's __mul__()
dunder method. This tells the interpreter how to deal with multiplication when an instance of SubstackArticle
is the first operand. I'm also defining the class's __repr__()
special method so that you can identify the object when you print it out (read more about __repr__()
in this Real Python article I wrote recently):
The __mul__()
special method checks whether other
is an integer. If it is, the method returns a new SubstackArticle
instance with the word count multiplied by the value of other
. We'll return to this method later to deal with the case when other
is not an integer.
Since __mul__()
is defined, my_article * 3
is valid. It returns a new article with 3000
words since the original article had 1000
words.
However, 3 * my_article
still doesn't work because the __mul__()
method in the integer class cannot deal with a SubstackArticle
object. If you print 3 .__mul__(my_article)
, NotImplemented
is returned.
So, let's also define the reflected version of the multiplication special method in SubstackArticle
:
Now that SubstackArticle
has the __rmul__()
method defined, when 3 .__mul__(my_article)
returns NotImplemented
, my_article.__rmul__(3)
is called.
You defined __rmul__()
to behave in the same way as __mul__()
. Therefore, both multiplications return a SubstackArticle
object with 3000
words. By defining both __mul__()
and __rmul__()
, you enabled the multiplication of a SubstackArticle
with an integer to be commutative—it doesn't matter which comes first in the multiplication.
Let's add one more temporary line to ensure we can follow when __rmul__()
is called. Note the extra print()
function call in __rmul__()
:
This version confirms that __rmul__()
is called when the first (or left) operand is an integer since 3 .__mul__(my_article)
returns NotImplemented
.
Can you also multiply a SubstackArticle
by other types?
Let's try to multiply the SubstackArticle
instance by a string:
This doesn't work. However, this behaviour is not desirable. Both multiplications with strings returned None
. It shouldn't be surprising that both returned the same value since you defined both __mul__()
and __rmul__()
. However, you want this to raise an error and not return None
.
The __mul__()
special method checks whether other
is an integer. However, when an object of a different data type is used as the second operand in a multiplication, the method returns None
. Recall that functions will always implicitly return None
if there's no return
statement.
You can fix this by returning NotImplemented
for all scenarios other than integers:
Now, the code raises a TypeError
when you attempt to multiply a SubstackArticle
instance with a string or any other data type that is not an integer.
Final Words
As is often the case, you'll find more detail waiting to be discovered when you dig a bit deeper beneath the surface of any Python topic. In this article, you explored a few examples of what happens when you add or multiply objects of different types.
The reflected operand version of __add__()
is __radd__()
and the reflected operand version of __mul__()
is __rmul__()
. I'm sure you've cracked the code behind the naming of these reflected operand versions! Most binary arithmetic operations can have a method to deal with reflected operands. You can see the full list here.
Next time you define some of these special methods in a class, you may want to consider whether you also need the reflected operand version.
Code in this article uses Python 3.11
Stop Stack
It was hard to decide what the first article on The Python Coding Stack should be. Then I just stopping thinking about it and published this one. If you're new to my articles, you'll soon see that the type of articles I write can be varied. Some focus on specific corners of Python programming, such as this one. Others will be broader projects. And sometimes there will be higher-level analogies and discussion.
You may be wondering what's Stop Stack? It's a quick way for me to communicate with you. At the end of each article I'll include a few short bullet points that are unrelated to the main article.
I'm looking forward to running the first cohort of The Python Coding Programme later this month. Live sessions with very small cohorts over 3 weeks, with 90 minutes live on Zoom every day (4 days a week). Each cohort only has 4 participants and there's active mentoring throughout with a private forum for the cohort to continue discussions. These cohorts are for those at the start of their programming journey. If you know anyone interested, here's some more information about The Python Coding Programme for Beginners.
Coming soon on The Python Coding Stack: A series of articles about classes and object-oriented programming which build on the Hogwarts School of Codecraft and Algorithmancy Twitter series which some of you may have seen.
Have you tried using Substack's new Notes? It's a bit like Twitter, yes, but it feels as though the interactions will be different and that Notes will have its own vibes. I'll be there as I try out this new platform. Hope to chat with you there.
Substack’s code blocks are not ideal at the moment. Hopefully they’ll improve soon. The best compromise I’ve found is to include the code in properly formatted and syntax-highlighted image snippets instead of Substack’s native code blocks. All images with code have the text of the code available in the image ALT text. The longer code segments also have a link (as a caption) to a GitHub Gist to make copying the code easier. Let me know your thoughts…
I applaud, once again, your efforts concerning making code both pleasing to look at and easy to access as text.
One question: how does one access the alt text of one of your shorter snippets? I can see the alt text, in a tool-tip sort of window, when I hover over the image, but I don't see a way to copy it. (Unlike, say, Twitter's pop-up that comes when clicking the "Alt" button.) Yes, I can get to it by doing View Source and searching for "alt=", but I presume you have something less clunky in mind.
Not that I need to do this, right here and now, but I thought it might be worth asking, as a general matter.
If the answer is, "There is no good way; that's why I use the 'copy code' links to GitHub, and anything that's too short to merit a link should be NBD for you to type in yourself", that's perfectly fine!
I do hope Substack does something about improving the style of embedded code. That would be big.
Nice explanation on the r methods with good illustrative case.