In simple terms, Python magic functions (also known as dunder methods) are methods that have double underscores at the beginning and end of their names. When defined inside a class, the Python interpreter automatically invokes them in specific situations. This allows you to customize your classes for various operasions, tailored to your own use cases.
Let’s start with a small example. Suppose we want to make a class iterable so we can loop over its contents. The typical, manual approach might look like this:
team = Team(["alice", "bob", "charlie"])
for member in team.members: print(member)
</div>By implementing the `__getitem__` method, we can turn a class into an iterable (a sequence type). The `item` argument corresponds to the index, and the way it behaves depends on the sequence type you want to mimic (list, dict, tuple).
<div>```
class Team:
def __init__(self, members):
self.members = members
def __getitem__(self, idx):
return self.members[idx]
team = Team(["alice", "bob", "charlie"])
partial_team = team[:2]
for member in partial_team:
print(member)

How the Python Data Model Influences Syntax
By using the Python data model (i.e., magic functions) inside a class, you effectively change the language’s syntax for that class. For example, defining __getitem__ turns the class into a sequence, allowing slicing and indexing. Implementing __len__ makes the len() function work on its instances.
def __getitem__(self, idx):
return self.members[idx]
def __len__(self):
return len(self.members)
team = Team(["alice", "bob", "charlie"])
print(team[:2]) print(len(team))
</div>Output:

### Attribute Interceptor: `__getattr__`
`__getattr__` is only called when an attribute (or method) is not found through normal lookup. If the attribute or method exists on the object, `__getattr__` is not invoked.
<div>```
class ClientProxy:
@staticmethod
def as_operator(username):
return OperatorService().as_operator(username)
def __getattr__(self, name):
return getattr(OperatorService(), name)
class OperatorService:
def __init__(self):
self.operator = None
def as_operator(self, operator):
self.operator = operator
return self
def delete_user(self, pk):
print('delete_user ok')
return True
def list_operators(self):
print('listing operators')
if __name__ == '__main__':
ClientProxy().as_operator('john').delete_user('123')

A simple real‑world scenario – data base operations:
class DatabaseInvocation:
def __init__(self, operation):
self.operation = operation
def __call__(self, *args, **kwargs):
backend = DatabaseBackend()
func = getattr(backend, self.operation)
try:
return func(*args, **kwargs)
except Exception as e:
print(e)
class DatabaseBackend: '''Manage user data in the database'''
def add_user(self):
print('add_user')
def update_user(self):
print('update_user')
def remove_user(self):
print('remove_user')
def fetch_user(self):
print('fetch_user')
def is_last_admin(self):
print('is_last_admin')
if name == 'main': DatabaseProxy().remove_user() DatabaseProxy().fetch_user()
</div>Output:

Combining the two patterns:
<div>```
class ClientProxy:
@staticmethod
def as_operator(username):
return OperatorService().as_operator(username)
def __getattr__(self, name):
return getattr(OperatorService(), name)
class OperatorService:
def __init__(self):
self.operator = None
self.database = DatabaseProxy()
def as_operator(self, operator):
self.operator = operator
return self
def add_user(self):
self.database.add_user()
def remove_user(self):
self.database.remove_user()
def update_user(self):
self.database.update_user()
class DatabaseProxy:
def __getattr__(self, operation):
return DatabaseInvocation(operation)
class DatabaseInvocation:
def __init__(self, operation):
self.operation = operation
def __call__(self, *args, **kwargs):
backend = DatabaseBackend()
func = getattr(backend, self.operation)
try:
return func(*args, **kwargs)
except Exception as e:
raise e
class DatabaseBackend:
'''Manage user data in the database'''
def add_user(self):
print('add_user')
def update_user(self):
print('update_user')
def remove_user(self):
print('remove_user')
def fetch_user(self):
print('fetch_user')
if __name__ == '__main__':
ClientProxy().as_operator('admin').add_user()
ClientProxy().as_operator('admin').update_user()
ClientProxy().as_operator('admin').remove_user()
ClientProxy().remove_user()
Magic functions can be grouped into two categories: non‑numerical and numerical operations.
Non‑numerical magic functions


Difference between __str__ and __repr__
Both methods are used for displaying a object. __str__ is intended for end‑users, while __repr__ is aimed at developers. __str__ is called during string formatting, e.g., by print(obj). __repr__ is used in all other contexts, such as in the interactive console or by the built‑in repr() function. When you type an object’s name directly in the interpreter, the displayed description comes from __repr__.

Numerical magic functions
