Adding a dunder to an object

Sunday 5 June 2022

We had a tricky debugging need at work: we wanted to track how an attribute on an object was changing. Here’s the unusual solution we used.

The __setattr__ special method (dunder) is called when an attribute is changed on an object. But like all special methods, it’s not found on the object, only on the class. We didn’t want to track changes to all objects of the class because it would produce too much noise in the logs.

So we wanted a per-object special method. The way we came up with was to define a new class with the special method. The class is derived from the object’s existing class, so that everything would work the way it should. Then we changed the object’s class.

Changing an object’s class sounds kind of impossible, but since in Python everything happens at run-time, you can just assign a new class to obj.__class__, and now that is the object’s class.

Here’s the code, simplified:

>>> class SomeObject:
...     ...

>>> class Nothing:
...     """Just to get a nice repr for Nothing."""
...     def __repr__(self):
...         return "<Nothing>"

>>> obj = SomeObject()
>>> obj.attr = "first"
>>> obj.attr

>>> def spy_on_changes(obj):
...     """Tweak an object to show attributes changing."""
...     class Wrapper(obj.__class__):
...         def __setattr__(self, name, value):
...             old = getattr(self, name, Nothing())
...             print(f"Spy: {name}{old!r} -> {value!r}")
...             return super().__setattr__(name, value)
...     obj.__class__ = Wrapper

>>> spy_on_changes(obj)
>>> obj.attr = "second"
Spy: attr: 'first' -> 'second'

>>> obj.attr

>>> obj.another = 'foo'
Spy: another: <Nothing> -> 'foo'

One more detail: the Nothing class lets us use repr() to show an object but also get a nice message if there wasn’t a value before.

The real code was more involved, and showed what code was changing the attribute. This is extreme, but helped us debug a problem. As I said in Machete-mode Debugging, Python’s dynamic nature can get us into trouble, so we might as well use it to get us out of trouble.


Add a comment:

Ignore this:
Leave this empty:
Name is required. Either email or web are required. Email won't be displayed and I won't spam you. Your web site won't be indexed by search engines.
Don't put anything here:
Leave this empty:
Comment text is Markdown.