What if our instances should be able to be used with mathematical operations? We can define that!

class NumString:
    def __init__(self, value):
        self.value = str(value)

    def __str__(self):
         return self.value

    def __int__(self):
        return int(self.value)

    def __float__(self):
        return float(self.value)
    
    def __add__(self, other):
        if '.' in self.value:
            return float(self) + other
        return int(self) + other
      
    def __radd__(self, other):
        return self + other
    
    def __iadd__(self, other):
        self.value = self + other
        return self.value
    
    def __mul__(self, other):
        if '.' in self.value:
            return float(self) * other
        return int(self) * other
    
    def __rmul__(self, other):
        return self * other
    
    def __imul__(self, other):
        self.value = self * other
        return self.value

__init__
In this program, the job of the init method is to get a value, turn it to string, and assign it to an attribute.

__str__
Is called when the object is converted to string.

__int__
Is called when the object is converted to integer.

__float__
Is called when the object is converted to float.

__add__
Is called when the object is added to a value using the + operator.

__radd__
Is called when the object is added to a value being on the other side of the +operator.

__iadd__
Is called when the object is added in place, using += operator.

__mul__
Is called when the object is multiplied by a value using the * operator. __rmul__and __imul__ work the same way their counterpart addition methods work.