Magic Methods and Attributes in Python
Python provides special methods (called magic methods or dunder methods) that customize the behavior of objects.
They are surrounded by double underscores (__), like __init__, __new__, and __call__.
Let’s explore the important ones:
__new__
- Responsible for creating a new instance.
- Called before
__init__. - Rarely overridden (except in immutable objects like
tuple,str).
class MyClass:
def __new__(cls):
print("Creating instance")
return super().__new__(cls)
def __init__(self):
print("Initializing instance")
obj = MyClass()
__init__
- Initializes the object after it is created.
- Commonly used to set initial attributes.
class Person:
def __init__(self, name):
self.name = name
p = Person("Alice")
print(p.name) # Output: Alice
__del__
- Called when an object is about to be destroyed.
- Can be used to clean up resources (e.g., files, network connections).
class FileHandler:
def __del__(self):
print("Closing file")
fh = FileHandler()
del fh
Note: Relying heavily on __del__ is discouraged because object deletion timing is unpredictable (especially with circular references).
__call__
- Makes an object callable like a regular function.
class Greeter:
def __call__(self, name):
print(f"Hello, {name}!")
greet = Greeter()
greet("Alice") # Output: Hello, Alice!
__slots__
- Limits the attributes that instances of a class can have.
- Saves memory by avoiding the creation of a
__dict__per object.
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
p.x # 1
- Trying to assign an attribute outside
__slots__will raise anAttributeError.
__dict__
- A dictionary containing all the writable attributes of an object.
class Car:
def __init__(self, brand):
self.brand = brand
c = Car("Toyota")
print(c.__dict__) # Output: {'brand': 'Toyota'}
- Used for introspection, dynamic attribute assignment, and debugging.
Summary Table
| Magic Method | Purpose |
|---|---|
__new__ |
Create a new instance |
__init__ |
Initialize a new object |
__del__ |
Cleanup before destruction |
__call__ |
Make an object callable |
__slots__ |
Optimize memory usage |
__dict__ |
Store object attributes |
Mastering magic methods helps in writing more efficient, customizable, and clean Python code!
Let me know if you’d like examples of custom __str__, __repr__, or operator overloading next!
Interview angle
- “
__new__versus__init__?” —__new__allocates and returns the instance;__init__initialises the already-created one. You override__new__for immutable types (subclassingtupleorstr), singletons, or when returning a different class. If__new__returns something that isn’t an instance of the class,__init__never runs. - “When would you use
__slots__?” — to remove the per-instance__dict__when you have very many small objects. It cuts memory substantially and speeds attribute access, at the cost of no dynamic attributes and complications with multiple inheritance. Measure before reaching for it. - “Why is
__del__unreliable?” — it runs when the refcount hits zero, which may be never (reference cycles) or at interpreter shutdown with a half-torn-down environment. Exceptions inside it are swallowed. Use a context manager orweakref.finalizefor deterministic cleanup. - “
__repr__or__str__?” —__repr__is for developers and should ideally be unambiguous enough to reconstruct the object;__str__is for users. Define__repr__always — it’s what you see in logs, tracebacks and the REPL, and the default is useless.