text: The magic methods in Python programming language are specifically for Object Oriented Design. Every class that we create has its own magic methods. Python’s standard interpreter assigns these to every class we create inside it. So, in this article, we shall see in detail how to call and use magic methods for a better programming approach. Let the coding fun begin!
  Brushing up OOP knowledge
  Before we get to the main topic, let us understand and polish the knowledge of OOP concepts. We shall see only the basics. So, Object Oriented Programming is a way of enclosing the data members and member functions into a user-defined entity called a Class.
  Class is something that holds particular data items that relate to each other and communicate in a specific way. We access the properties and the member functions using Object. The object is an instance of a class. In any programming language, memory is never allocated when we create a class, but it is actually created when we create its instance I.e object.
  Example:
  The animal is a type of class. In that, we include all the living beings that reside on Earth. So, everyone has their own way of living, food, and shelter. The animal just defines the blueprint of all these. For example, the cat is the object of the Animal class. It has four legs, eats mice, and lives in houses or bushes. In the same way, the tiger has four legs but it kills and eats many animals so we say that the tiger eats meat, it lives in the forest.
  Code Example with Python:
  class Animal: def __init__(self, legs, food, shelter): self.legs = legs self.food = food self.shelter = shelter def showAnimal(self): print("The animal has {} legs: ".format(self.legs)) print("The animal eats: {}".format(self.food)) print("The animal lives in: {}".format(self.shelter)) cat = Animal(4, "Mouse", "House") tiger = Animal(4, "Meat", "Forest") cat.showAnimal() tiger.showAnimal()
  Output:
  The animal has 4 legs: The animal eats: Mouse The animal lives in: House The animal has 4 legs: The animal eats: Meat The animal lives in: Forest
  Explanation:
  - The animal class contains the number of legs, food, and shelter as properties.
  - When we create an instance and insert the values inside the constructor, the difference in their behaviour is clear.
  - So, objects of the same class can be different according to the behaviour of values.
  The magic methods in OOP
  So, in the above example, we have a class as Animal. Python has a set of methods namely Dunder methods that are responsible for holding the properties, data members, and member functions of a class.
  Definition: When we create an object, the Python interpreter calls special functions in the backend of code execution. They are known as Magic Methods or Dunder Methods.
  Why do we say Dunder? Because their names lie between double underscores. They perform some set of calculations that are just like magic while we create an object of a class. So, how do we check what are they and how many of them are there in the standard class? Use the steps below to find them:
  - Create a sample class.
  - Create its object.
  - Use the dir() function and insert the object inside it.
  - This function prints a list of all the Magic Methods along with data members and member functions that are assigned to the class.
  Code:
  print(dir(cat))
  Output:
  __class__ __delattr__ __dict__ __dir__ __doc__ __eq__ __format__ __ge__ __getattribute__ __gt__ __hash__ __init__ __init_subclass__ __le__ __lt__ __module__ __ne__ __new__ __reduce__ __reduce_ex__ __repr__ __setattr__ __sizeof__ __str__ __subclasshook__ __weakref__ food legs shelter showAnimal
  The names that you see in double underscores are all magic methods. The rest of the properties are defined by the user. As we can see
  __init__() which is the constructor of any class in Python, is also a magic method. Let us see their use one by one. To understand their functionality always try to override the functions.
  One thing to note is that, for any class the user defines, there is some set of default magic methods with respect to each of them.
  Use and implementation of some magic methods
  In this section, we shall see the use and implementation and use of some magic methods for writing a better OOP design.
  1. __new__():
  This method helps the constructor __init__() method to create objects for a class. So, when we create an instance of a class, the Python interpreter first calls the __new__() method and after that__init__() method. They work hand in hand with each other.
  - When a programmer opts to create an object, __new__() gets invoked that accepts the name of the object.
  - Then __init__() is invoked where the parameters including self are inserted into the object which in turn helps us to modify the class properties.
  Code:
  class Sample: def __new__(self, parameter): print("new invoked", parameter) return super().__new__(self) def __init__(self, parameter): print("init invoked", parameter) obj = Sample("a")
  Output:
  new invoked a init invoked a
  Explanation:
  - First, we create a class as Sample.
  - Then override the __new__() method by creating it. Then, as usual, the self parameter comes, and after that gives a simple parameter.
  - Return a super() function with the
  __new__()function with the self parameter to get access to the customizations we make to the method.
  - Then, with the same practice call the
  __init__()function with some parameter.
  - After that create an object of a sample class.
  - Now, when we run the code, the interpreter first calls the __new__(), and then it calls __init__() method.
  2. __init__():
  Python is an Object Oriented Programming language. So, the class must have a constructor. This requirement is fulfilled using the __init__() method. When we create a class and want to give some initial parameters to it. The initializer method performs this task for us.
  Code:
  class Sample: def __init__(self, parameter): print("init invoked", parameter) obj = Sample("a")
  Output:
  init invoked a
  Explanation:
  - Create/override the __init__() method. Insert self parameter to signal the interpreter that this is a class method.
  - Insert required parameter.
  - Then print that parameter using the print() function.
  - After that create an object.
  - When we run the code we get the output as “init invoked a”, this states that the interpreter calls init() and prints that parameter.
  3. __str__():
  This method helps us to display the object according to our requirements. So, let us say that when we create an object and try to print it. The print() function displays the memory location of the object. If we want to modify we can do this. The __str__() function gives a nice representation of the object.
  Code (before using __str__()):
  class Student: def __init__(self, name, roll_no): self.name = name self.roll_no = roll_no stud_1 = Student("Suresh", 1) print(stud_1)
  Output:
  <__main__.Student object at 0x0000023E2CF37CA0>
  Code (after using __str__()):
  class Student: def __init__(self, name, roll_no): self.name = name self.roll_no = roll_no def __str__(self): return ("{} {}".format(self.name, self.roll_no)) stud_1 = Student("Suresh", 1) print(stud_1)
  Output:
  Suresh 1
  Cool right! now we can also use similar methods. We can format the object as according to our needs.
  4. __repr__():
  Similar to the __str__(), we can use __repr__ function for the decoration of objects. The code is similar to __str__() implementation.
  class Student: def __init__(self, name, roll_no): self.name = name self.roll_no = roll_no def __repr__(self): print("repr invoked") return ("{} {}".format(self.name, self.roll_no)) stud_1 = Student("Suresh", 1) print(stud_1)
  Output:
  repr invoked Suresh 1
  5. __sizeof__():
  When we create a class the interpreter never assigns memory to it. It assigns memory to the object. If we want to know the memory allocated to that object, then we can call or override the __sizeof__() function and pass our object. This also returns size of a list =, tuple, dictionary object.
  Code:
  class Student: def __init__(self, name, roll_no): self.name = name self.roll_no = roll_no stud_1 = Student("Suresh", 1) print("Size of student class object: ", stud_1.__sizeof__()) list_1 = [1, 2, 3, 4] tup_1 = (1, 2, 3, 4, 5) dict_1 = {"a":1, "b":2, "c":3, "d":4} print("Size of list: ", list_1.__sizeof__()) print("Size of tuple: ", tup_1.__sizeof__()) print("Size of dictionary: ", dict_1.__sizeof__())
  Output:
  Size of student class object: 32 Size of list object: 104 Size of tuple object: 64 Size of dictionary object: 216
  6. __add__():
  This magic method is specifically similar to its name. It adds two variables. For an integer it returns the sum, for a string, it returns their concatenation result.
  Code:
  class Numbers: def __init__(self, a, b): self.a = a self.b = b def __add__(self): print("__add__ invoked") return self.a + self.b num = Numbers(3, 4) num_2 = Numbers("a", "b") print(num.__add__()) print(num_2.__add__())
  Output:
  __add__ invoked 7 __add__ invoked ab
  7. __reduce__():
  This magic method returns a set or a dictionary of all the parameters of a class and their values in key: value format. This can be directly called using the object name with the dot operator. So, when we create a class and instantiate it with some values. The function shall return it with the name of the parameters which were given during the declaration of a class.
  Code:
  class Employee: def __init__(self, name, salary): self.name = name self.sal = salary emp = Employee("Shrinivas", 150000) print(emp.__reduce__())
  Output:
  (<function _reconstructor at 0x0000023E22892EE0>, (<class '__main__.Employee'>, <class 'object'>, None), {'name': 'Shrinivas', 'sal': 150000})
  Code (after overriding __reduce__()):
  class Employee: def __init__(self, name, salary): self.name = name self.sal = salary def __reduce__(self): return self.name, self.sal emp = Employee("Shrinivas", 150000) print(emp.__reduce__())
  Output:
  {"Shrinivas", 150000}
  Explanation:
  When we override and try to return the parameters, we only get their values in a set.
  8. __hash__():
  The __hash__() function returns a specific hash value of the object stored in the heap memory. We can either override it or call it using the object name. Hashing is very useful to fetch the memory address of any random element in a computer. All programming languages use hash for the sake of simplicity and for memory allocation.
  Code:
  class Employee: def __init__(self, name, salary): self.name = name self.sal = salary def __hash__(self): return super().__hash__() emp = Employee("Shrinivas", 150000) print(emp.__hash__())
  Output:
  154129100057
  Code:
  class Employee: def __init__(self, name, salary): self.name = name self.sal = salary emp = Employee("Shrinivas", 150000) print(emp.__hash__())
  Output:
  154129054082
  9. __getattribute__(name):
  This function returns the value of the attribute of a class if it exists. We need to call the function and pass the attribute which we assigned to the class parameter using the self keyword. Like if we assign the value of salary to self.sal we need to call sal inside the __getattribute__() function.
  Code:
  class Employee: def __init__(self, name, salary): self.name = name self.sal = salary def __getattribute__(self, name): return super().__getattribute__(name) emp = Employee("Ravi", 500000) print(emp.__getattribute__("sal"))
  Output:
  50000
  Explanation:
  In this function, the “self.sal” is assigned to the salary parameter of the Employee class. The function returns its value as the attribute that exists inside the class. If it does not exist the function returns an error message.
  10. __setattr__(name, value):
  As the name suggests, this magic method helps us change the value of an attribute when we define the object. No need to override the __getattribute__() and __setattr__() functions. Just call them using the objects created.
  Code:
  class Employee: def __init__(self, name, salary): self.name = name self.sal = salary emp = Employee("Ravi", 500000) emp.__setattr__("name", "Suresh") emp.__setattr__("sal":600000) print("The changed name of the employee is: ", emp.__getattribute__("name")) print("The changed salary of the employee is: ", emp.__getattribute__("sal"))
  Output:
  The changed name of the employee is: Suresh The changed salary of the employee is: 600000
  Explanation:
  - the __setattr__() take two parameters.
  - name of attribute
  - its new value
  - Then it assigns that particular value to that attribute.
  - After that, to check the value assigned to it call the __getattrbute__() function using the employee object and dot operator. emp.__getattribute(“name”).
  Point to note: These two functions replace getter and setter methods for a class in Python.
  Conclusion
  So, we saw the in-depth implementation of some of the magic methods in Python. I hope this helps and will make programming easier. They prove to be helpful pieces of code for quick implementation and use. Happy python programming 🐍🐍😎.