Main image of article Exploring Magic Methods in Python 3 is Vital for Programmers
The Python documentation, although generally pretty good, lacks a reference list to so-called “magic methods.” A method is the name for a function in a Python class, and many built-in methods are “magic” because you never call them directly; instead, they are invoked from Python as an indirect effect of another call. (A full listing of magic methods is surprisingly long, so this article is about a small subset of them.) Magic methods are easily identified by having double underscores in front and back, such as __init__. Instead of saying double underscores each time, it's a convention to call them “dunders.” The other thing that’s magic about them is that they are provided with every class, and you can extend them by defining them in your class and changing the built-in functionality. This sort of behavior is typical of other programming languages such as C++, C#, and Java; but if you’re new to classes in Python, it’s a brave new world.

Classy Python

You can do a lot with Python without ever having to use classes; but sooner or later you will need to use them. Python lets you define your own classes, inherit from built-in classes, and create instances of your classes. You define a simple class like this; it has a single instance variable “y.”
class ClassName:
    self.y = 10

Underscores and Dunders

Python really likes underscores. For instance, if you prefix a member with a single underscore, it tells the world that it's off-limits (i.e. inaccessible). A member is the name of a method (i.e. function) or variable defined in the class. Now, back to the dunder. If you put a dunder (__) just at the start of a member, it obscures it so it can't be accessed from outside the class. It's not a substitution for being off-limits; it's the same as a sealed method in C#. You can't override it in a descendant class. Underscores are really a syntactic sugar to help you understand what you can and can't do. With a member identifier x (methods or variables), if you use __ on the front of it, then it's treated as if it were _classname_x. But when you see dunders at the front and back then you’ve got a magic method.

Magic Methods in Python

The first one is __new__, and it's a constructor that is called automatically when you create an instance of a class. A constructor is a method that is responsible for creating an instance of a class. It's not the easiest to work with, but lets you control creation of the class. If you don’t supply it, Python does it for you. You'll see the words “self” and “cls” used in the examples below. When you create an instance of a class, you refer to it using “self” (this is a convention more than a hard and fast rule). Likewise with “cls,” which refers to the class itself (the Point class in the example below), not an instance of it (like P below).
from datetime import datetime

class Point:
    def __new__(self):
        print("Class {} created at {} ".format(self.__name__,str(datetime.now())) )

p = Point()
  This prints something like: "Class Point created at 2018-09-16 11:39:38.827747". The __name__ is another magic method that returns the name of the class.

__init__

This is known as an initializer, and is called after __new__. It’s where you should initialize the instance variables:
class Point:

    def __new__(cls,_x,_y):
        print("__new__ called x=",_x," y=",_y)
        return super(Point,cls).__new__(cls)

    def __init__(self, x, y):
        print("__init__ called x=",x," y = ",y)
        self.x = x
        self.y = y

p=Point(1,2)
_p = Point(10,20)
print("p.x=",_p.x," p.y = ",_p.y)
  That program prints out:
__new__ called x= 1  y= 2
__init__ called x= 1  y =  2
__new__ called x= 10  y= 20
__init__ called x= 10  y =  20
p.x= 10  p.y =  20
  Even when you don't define __new__, the __Init__ method is still called. If you comment out the three lines that define __new__, then you'll get the same output without the two "__new__ called..." lines.

__del__

Just as __init__ inits a class similar to a constructor, __del__ is like a destructor. If you've opened a file in __init__, you'd close it here. Python includes a built-in garbage collector, so __del__ is needed far less in Python programs than a destructor (say, in C++). __del__ can even get called after the program has ended; that’s garbage collectors for you! Objects don't get cleaned up by the Python garbage collector as an object goes out of scope; just when the last reference to it has gone out of scope. Unless you have a special need, you can generally manage without __del__.

__call__

Another method that's not needed quite as often is __call__. If defined in a class, then that class can be called as if it were a function, rather than modifying the instance itself. As functions are first class objects in Python, they can be passed to other functions. In the example below, I’ve used them a bit like a closure.
class Point:

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __call__(self):
        return self.x + self.y

p1 =Point(1,2)
p2 = Point(10,20)
print("p1()=",p1())
print("p2()=",p2())
  The output is:
p1()= 3
p2()= 30

Iterations

One of Python’s strengths is its support for iterators; you can use them in your classes. To create an iterator class, you need to define __iter__ and __next__, plus __reversed__ if you wish to iterate in reverse order.
class Poly:

    def __init__(self, a, b,c, iterations):
        self.a = a
        self.b = b
        self.c = c
        self.iterations = iterations

    def __iter__(self):
        self.i = 0;
        return self

    def __next__(self):
        self.i += 1
        if self.i > self.iterations:
            raise StopIteration
        return self.a*(self.i*self.i)+ (self.b*self.i)+ self.c


for n in Poly(1,23,10,5):
    print(n)
  This outputs:
34
60
88
118
150
  This class evaluates the nth iterations, where it calculates the polynomial = a * x * x + b * x + c for x in 1 to 5. The __iter__ methods resets the start of the sequence, and the __next__ is repeatedly called until the sequence variable (self.i) exceeds the specified number of iterations (self.iterations).

Conclusion

Python offers you the choice of writing procedural code and/or object-oriented, and once you are creating non-trivial applications, you’ll find it easier moving away from purely procedural code. Objects simplify enormously the handling of complicated data structures. To get the best out of Python classes you need to know at least one magic method __init__ and you should take advantage of the others I’ve looked at. I’ve barely scratched the surface with this subset of the available magic methods. For example, there are comparison operators so you can compare instances of your objects for equality, numeric methods for abs, invert, round and many more. Add to that additions, and you can implement your own addition (like concatenation of strings with +), type conversion, containers, reflection and serializing (pickling). These are more advanced, though, so start with simple classes and then start looking at the other magic methods.