
What are magic methods?
They’re everything in object-oriented Python. They’re special methods that you can define to add "magic" to your classes. They’re always surrounded by double underscores, for example__init__. They’re also not as well documented as they need to be. All of the magic methods for Python appear in the same section in the Python docs, but they’re scattered about and only loosely organized. There’s hardly an example to be found in that section.
To rectify a perceived lack of documentation on magic methods, I have complied some plain-English examples of how to use Python’s magic methods.
Creating a dict object that can only accept integers and floats as its values
To begin, lets create a fictitious use case. In this scenario, I would like to create a class that creates dictionary objects, that only accept either integers or floats as their values.
If any other data types, for example; strings, lists, tuples are added as a value to our custom dictionary object, an exception will be raised, specifying to the user that this custom dict object can only accept integers and floats as it’s values.
In order to implement this, we will make use of the following magic methods:
int, setitem, and str
To begin, I first create a custom class called CustomIntFloat, and I pass dict into the argument inheritance list. This means, that the object that we create will behave exactly like a dictionary, except in the places where we choose to selectively modify this behavior.
I then create an init method to construct my CustomIntFloat dict object. This object takes a key and value in it’s argument list, which I have set to the None type by default. The reason for this being, is that, if a user creates an object of the CustomIntFloat class, without passing a key or value, an empty dict will be created. I create a simple conditional which says: if the key is not passed, the key parameter is assigned the argument None by default, and an empty dict is created by referencing the CustomIntFloat object, with the class attribute empty_dict.
Following on, if the user specifies a key of length one, and a corresponding value that is an instance of the int or float class, the key and the value will be set in the object.
Finally, in the else statement, if the user specifies multiple keys and values as an iterable, the iterables will be zipped by the zip function and assigned the variable name zipped. I loop over zipped, and check that the value is of type int or float. If it is not, a custom CustomIntFloatError exception is raised.

The CustomIntFloatError Exception class and the str magic method
When we raise a CustomIntFloatError exception, we are really creating an instance of the CustomIntFloatError class and printing it at the same time.
As such, this custom error class needs init and str magic methods. The instance we create takes the value passed, and sets it as an attribute value in the CustomIntFloatError class.
This means, when an error message is raised, the value passed to the init of CustomIntFloat can be set as an attribute (self.value)in the CustomIntFloatError class and easily inspected.

When invalid input is specified, the CustomIntFloatError exception is raised and the object is not constructed. A useful error message informs the user that only integers and floats are valid values.

Similarly, when you attempt to instantiate the object z (that has been commented out), with multiple keys and values, the same exception will be raised, informing the user that ‘three’ is not valid input.
The setitem Magic Method
setitem is a magic method that is invoked when we set a key and a value in the dictionary. Once the CustomIntFloat object has been constructed, if the user attempts to add a value that is not of type int or float, the same CustomIntFloatError exception will be raised. I have included a code snippet below that shows how to set a key and a value as intended.

Invalid input results in a CustomIntFloatError exception being raised as shown below.

For those interested, the source code for the code presented can be found on my GitHub page here:
Summary of the CustomIntFloat Class
By inheriting through built-in classes like the dict, we can customize our behavior through the re-implementation of magic methods. This has many benefits.
Importantly, the user does not need to learn any new syntax. When the user wants to add a key and a value to the CustomIntFloat dict object, they just add it in the usual way. The only difference is that we have selectively chosen to allow only values of type int and float. If the user specifies any other type, a custom error message usefully instructs them where they went wrong, and what type of values are expected.
Quick-Fire Magic Methods
Examples using Mathematical Operators
sub, add, and mul (with a custom repr)
We can also take advantage of the mathematical operators in Python with magic methods. Lets see how we can use magic methods, like add, sub, and mul in our own custom object that we create.
Operators, like +, -, /, and * are polymorphic methods. As can be shown below in the Python prompt, the plus sign (+), is polymorphic, and can be used to concatenate strings, sum up integers, and combined lists together. This is possible, because all these types, str, list, and int have an add method in their respective classes. Python simply converts the + sign to an add method call on the object that called it (see examples below).
This means we can take advantage of the + sign in our own objects if we include a add method in our class.

Magic Method Operator Methods in Our Class
Here, we are creating a class called NumOperations. This class creates NumOperations objects. When a user of this class, passes a list to the arguments list of the init, that list is set as an attribute in the NumOperations object and named, .math_list.
When the NumOperations object(s) have been constructed, we can conveniently utilize the magic methods to work with these custom objects and broadcast a mathematical operation.
To illustrate, the magic method sub takes 2 NumOperations objects, zips their lists together and iterates other their corresponding list of tuples. The second element in the tuple is subtracted from the first and that value is appended to a new list, called minuslst, and passed as the argument to the NumOperations constructor.
This now returns a new NumOperations object.
This broadcasting is being performed under the sub method. This means that we can take advantage of the minus (-) operator.
The magic method, repr has been re-implemented to return a string representation of the list set in the new object. This has been modified, so that when a user prints the output of two NumOperations objects, the user will see something which they might expect.
Here a list where the elements have been subtracted from one another;
[90, 81, 72, 63, 54].
Both add, and mul have a similar method implementation as sub, but use a list comprehension to cut down on the number of lines of code that are needed.

This broadcasting behavior is similar to that found in data analysis packages, such as Pandas and Numpy.
The add and mul methods are also made to work with two NumOperations objects. This means the user can take advantage of the plus operator, + and the multiplication operator . As can be seen, in the example below, q is the result of x y, which returns a new NumOperations object. When we print q, we get a string representation of the boradcasting operation as a list.

For interested parties, the source code is available by following the link to my GitHub gist.
Summary
We can make our custom designed objects work with Python operators, like the plus, minus and multiplication signs. This is very powerful functionality, and part of the power comes from the ability to communicate very easily what the object does. The user does not need to learn any new methods, they can simply add, subtract or multiply the NumOperations objects together and when they print the resultant object, they will see output that they might expect.




