I love Python. It’s a versatile, powerful language with a gentle learning curve, and you can do all sorts of cool things with it. However, it has some downsides, and one of them is that – as a language without explicit memory management – it can be hard to understand what’s happening to objects in memory during runtime.

Python has garbage collection, which means that within a running Python program, every now and then the runtime will scan through every object in memory and determine whether or not it’s still needed. If the garbage collector decides that the object is no longer necessary, it will remove it from memory. But this process is entirely abstracted from the programmer, and for those of us who want to know what’s actually going on in our software, it can be frustrating.

In this blog post, I’m going to demonstrate an impractical idea for gaining a little visibility into your Python runtime’s memory management: getting a text sent to your phone every time an object is removed from memory.

In order to do this, I’ll need a free Infobip account and the credentials that come along with it, and a terminal where I can run some Python code.

Setting up the Infobip SMS Channel

For the purposes of this blog post, I’m simply going to create a single Python file, garbage_texts.py. For this to work, I need the Infobip Python API SDK:

pip install infobip-api-python-sdk

Now, in my new garbage_texts.py file, I’ll set up an Infobip SMS Channel using the credentials from my account. I can access these credentials either from my Infobip account or from the Infobip API landing page, provided I have logged in.

from infobip_channels.sms.channel import SMSChannel

channel = SMSChannel.from_auth_params({
    "base_url": "<my_base_url>",
    "api_key": "<my_api_key>"

})

From now on, I can use this channel to send texts to my own phone number (registered during Infobip account creation) with the send_sms_message method.

Writing a class to send texts on deletion

Next, I’ll define a class that will text me whenever an instance of it is removed from memory. This is done by overriding the class’s __del__ method, the magic method that’s called by the Python runtime when it removes an object. This method is also called the finalizer.

class TextingClass:
	def __init__(self, name: str):
		# This class takes a name parameter so we can distinguish instances
		self.name = name

	def __del__(self):
		# Here’s where the object sends a text using the Infobip SMSChannel
		channel.send_sms_message({"messages":[{
	 		"destinations": [{"to": "<my_phone_number>"}],
			"from": "Python Runtime",
			"text": f"The object with name {self.name} is being removed from memory!"
		}]})

This class uses the Infobip SMS Channel to report when its instances are being removed from memory, so I can see what’s going on in my code.

To check that it works, I’ll add a script to  garbage_texts.py at the end of the file:

if __name__ == “__main__”:
	thing_one = TextingClass(“Thing One”)
	del thing_one

	import time
	time.sleep(500)

All this script does is create an instance of my TextingClass and then delete its namespace reference, thing_one. That causes its reference count to fall to zero, and that instance is removed from memory. Then the script waits to exit for 500 seconds, so that I can be sure that the text happened on the del line and not because the runtime exited when the script ends.

To run this code, all I need to do is the following:

python garbage_texts.py

Technically speaking, in this example the garbage collector wasn’t used at all. The garbage collector only comes into play when cyclic isolates are created, a situation in which objects contain references to each other but have no references from the namespace. I can easily set up a cyclic isolate with two elements in my script:

if __name__ == “__main__”:
	thing_one = TextingClass(“Thing One”)
	del thing_one
	
	thing_two = TextingClass(“Thing Two”)
	thing_three = TextingClass(“Thing Three”)

	# create cyclic references
	thing_two.reference = thing_three
	thing_three.reference = thing_two

	# isolate the cyclic reference objects from the namespace
	del thing_two
	del thing_tree 

	import time
	time.sleep(500)

If I run this code as is, I won’t get texts about Thing Two and Three being removed from memory until the script ends in 500 seconds. To remove them earlier, I need to run the garbage collector manually before the sleep call :

import gc
gc.collect()

In general, the garbage collector runs on its own automatically, and you can define how often you want that to happen if the default behavior isn’t right for your use case. In this case, I want to force it to run, which is what that code does.

Now, when I run my script, I’ll see texts for all three objects before the script exits.

Using decorators to send ALL the texts

So, I’ve shown how to write a single custom class that’ll send texts when instances of it are removed from memory. But what if I wanted all my custom Python objects to do this? That’d be a lot of repeated code, and we all know the rule about repeating yourself. Fortunately, I can write a decorator that will amend the __del__ method of any class to send a text when it’s called.

def text_on_delete(cls):
	# Get the original finalizer method
	base_del = getattr(cls, '__del__’')

	# Define a new one that’ll send texts
	def del_with_text(self):
		channel.send_sms_message({ "messages": [{
 		"destinations": [{
				"to": "<my_phone_number>"
			}],
			"from": "Python Runtime",
			"text": f"The object {self} is being removed from memory!"
		}]})
		# Call the original finalizer
		base_del(self)

	# Make sure the class uses our new finalizer
	setattr(cls, '__del__', del_with_text)
	return cls

Using this decorator, my TextingClass can look like this:

@text_on_delete
class TextingClass:
	def __init__(self, name: str):
		# This class takes a name parameter so we can distinguish instances
		self.name = name

A lot cleaner!

If you actually used this in a production system, you’d quickly get overwhelmed with text messages. However, you could modify this code to only send a text every 1000 objects, or to send an email instead. More practically, knowing how to hook into Python’s built-in magic methods and use decorators to achieve the behavior you want comes in handy all the time. This is a fun way to gain some visibility into what’s actually happening when you do so!