Python's del: Remove References From Scopes and Containers

Python's del: Remove References From Scopes and Containers

by Leodanis Pozo Ramos Apr 03, 2023 basics best-practices python

Python’s del statement will allow you to remove names and references from different namespaces. It’ll also allow you to delete unneeded items from your lists and keys from your dictionaries. If you want to learn how to use del in your Python code, then this tutorial is for you. The del statement empowers you to better manage memory resources in your code, making your programs more efficient.

In this tutorial, you’ll learn how to:

  • Write and use del statements in Python
  • Take advantage of del to remove names from different scopes
  • Use del for removing list items and dictionary keys
  • Remove object attributes using del
  • Write classes that prevent attribute deletion using del

To get the most out of this tutorial, you should be familiar with Python’s built-in data types, specifically lists and dictionaries. You should also know the basics of classes and object-oriented programming in Python.

Getting to Know Python’s del Statement

Python’s del statement allows you to remove references to objects from a given namespace or container type. It performs an operation that’s the opposite of what an assignment statement does. It’s sort of an unassignment statement that allows you to unbind one or more names from their referenced objects.

This is a natural feature to have in Python. If you can create a variable by writing variable = value, then you must have the option to undo this operation by deleting variable. That’s where del comes on the scene.

The del statement can come in handy in different coding situations, such as:

  • Freeing up memory resources
  • Preventing accidental use of variables and names
  • Avoiding naming conflicts

Of course, this list is incomplete. You may find some other appropriate use cases for this statement. In this tutorial, you’ll learn how the del statement works and how to use it in your code. To kick things off, you’ll start by learning the general syntax of del in Python.

Learning the del Syntax

A Python del statement consists of the del keyword, followed by a comma-separated series of references.

Here’s the general syntax of the del statement in Python:

del reference_1[, reference_2, ..., reference_n]

The del statement allows you to remove one or more references from a given namespace. It also lets you delete data from mutable container types, such as lists and dictionaries.

You’ll often use this statement with a single argument. However, it also supports a series of arguments separated by commas.

In the above construct, reference_* represents any kind of identifier that can hold references to concrete objects stored in memory. In practice, these references can include:

  • Identifiers, such as variables and names of functions, classes, modules, and packages
  • Indices of mutable sequences, such as a_list[index]
  • Slices of mutable sequences, like a_list[start:stop:step]
  • Keys of dictionaries, like a_dict[key]
  • Members of classes and objects, such as attributes and methods

You can use any of these references as arguments to del. If you use a comma-separated series of arguments, then keep in mind that del operates on every argument sequentially from left to right. This behavior can be risky when you’re removing items from lists, as you’ll learn later in this tutorial.

Here’s a quick example of using del to remove a variable:

>>> greeting = "Hi, Pythonista!"
>>> greeting
'Hi, Pythonista!'

>>> del greeting
>>> greeting
Traceback (most recent call last):
NameError: name 'greeting' is not defined

Once you’ve run the del statement, the variable greeting is no longer available. If you try to access greeting, then you get a NameError because that variable doesn’t exist anymore.

Removing reference holders like variables is the primary goal of del. This statement doesn’t remove objects. In the following section, you’ll dive deeper into the actual and immediate effects of running a del statement.

Understanding the Effects of del

As you’ve learned, Python’s del statement deletes references from namespaces or data containers. It doesn’t remove concrete objects. For example, you can’t remove literals of built-in types using del:

>>> del 314
  File "<input>", line 1
    del 314
SyntaxError: cannot delete literal

>>> del "Hello, World!"
  File "<input>", line 1
    del "Hello, World!"
SyntaxError: cannot delete literal

In these examples, note that you can’t use the del statement directly on objects. You must use it with variables, names, and other identifiers, as you already learned:

>>> number = 314
>>> del number

However, running del like in this example doesn’t mean that you’re removing the number 314 from memory. It only removes the name number from your current namespace or scope.

When you pass an identifier—like a variable, class, function, or method name—as an argument to del, the statement unbinds the identifier from the referenced object and removes the identifier from its containing namespace. However, the referenced object may not be deleted from your computer’s memory immediately.

Similarly, when you use a list index, slice, or dictionary key as an argument to del, you remove the reference to the target item, slice, or key from the containing data structure. This may not imply the immediate removal of the referenced object from your computer’s memory.

In short, del doesn’t remove objects from memory and free up the used space. It only removes references to objects. This behavior may raise a question. If del doesn’t remove objects from memory, then how can you use del to release memory resources during your code’s execution?

To answer this question, you need to understand how Python manages the removal of objects from memory. In the following section, you’ll learn about Python’s garbage collection system and how it relates to using del for freeing up memory in your programs.

Unraveling del vs Garbage Collection

Python has a garbage collection system that takes care of removing unused objects from memory and freeing up resources for later use. In Python’s CPython implementation, the reference count is the primary indicator that triggers garbage collection.

Essentially, Python keeps track of how many identifiers hold references to each object at any given time. The number of identifiers pointing to a given object is known as the object’s reference count.

If there’s at least one active reference to an object, then the object will remain accessible to you, occupying memory on your computer. If you use del to remove this single reference, then the object is ready for garbage collection, which will remove the object and free the memory.

On the other hand, if you have several active references to an object and you use del to remove some of these references but not all of them, then the garbage collection system won’t be able to remove the object and free up the memory.

In short, del removes references, while garbage collection removes objects and frees up memory. According to this behavior, you can conclude that del only allows you to reduce your code’s memory consumption when it removes the last reference to an object, which prepares the object to be garbage-collected.

It’s important to note that Python’s garbage collection system doesn’t free the memory used by an object immediately after you remove the last reference to that object, bringing the reference count down to zero. Instead, Python’s garbage collector periodically scans the memory for unreferenced objects and frees them.

When an object’s reference count reaches zero, the garbage collector may proceed to remove that object from memory. At that moment, the .__del__() special method comes into play.

Python automatically calls .__del__() when a given object is about to be destroyed. This call allows the object to release external resources and clean itself up. It’s important to note that the del statement doesn’t trigger the .__del__() method.

You won’t need to implement the .__del__() method in your own classes very often. The proper use of .__del__() is rather tricky. If you ever need to write this method in one of your classes, then make sure you carefully read its documentation page.

Using del vs Assigning None

Assigning None to a reference is an operation that’s often compared to using a del statement. But this operation doesn’t remove the reference like del does. It only reassigns the reference to point to None:

>>> number = 314
>>> number
>>> number = None
>>> print(number)

Reassigning the variable number to point to None makes the reference count of object 314 go down to zero. Because of this, Python can garbage-collect the object, freeing the corresponding memory. However, this assignment doesn’t remove the variable name from your current scope like a del statement would do.

Because None is a singleton object that’s built into Python, this assignment doesn’t allocate or use new memory but keeps your variable alive and available in your current scope.

The None approach may be useful when you want to prevent the NameError exception that happens when you attempt to delete a name or reference holder that doesn’t exist in your current namespace. In this case, Python will silently create a new variable and assign None as its initial value.

Deleting Names From a Scope

Removing a name from a given scope is the first use case of del that you’ll learn about in this tutorial. The scope of a given name or identifier is the area of a program where you can unambiguously access that name.

In Python, you’ll find at most four scopes:

  • The local, or function-level, scope
  • The enclosing scope of nested functions
  • The global, or module-level, scope
  • The built-in scope, where built-in names live

The del statement can remove names from some of these scopes. As you’ve seen in previous examples, you can use del to remove one or more variables from the global scope:

>>> color = "blue"
>>> fruit = "apple"
>>> pet = "dog"

>>> del color
>>> color
Traceback (most recent call last):
NameError: name 'color' is not defined

>>> del fruit, pet
>>> fruit
Traceback (most recent call last):
NameError: name 'fruit' is not defined
>>> pet
Traceback (most recent call last):
NameError: name 'pet' is not defined

In this example, you create three global variables. Then you use the del statement to remove these variables from your global scope, which is where they live. Remember that del has the effect of an unassignment statement.

You can also remove names from the local and enclosed scopes within your custom functions using del in the same way. However, you can’t remove names from the built-in scope:

>>> del list
Traceback (most recent call last):
NameError: name 'list' is not defined
>>> list()

>>> del dict
Traceback (most recent call last):
NameError: name 'dict' is not defined
>>> dict()

>>> del max
Traceback (most recent call last):
NameError: name 'max' is not defined
>>> max([3, 2, 5])

If you try to remove any built-in name using a del statement, then you get a NameError. The error message may seem confusing because you can access all these names as if they were in the global scope. However, these names live in the built-in scope, which isn’t directly accessible with del.

Even though you can’t delete built-in names, you can override or shadow them at any moment in your code. Python has many built-in names. Some of them may fit your naming needs in some situations. For example, when you’re beginning with Python, lists may be one of the first built-in data types that you learn about.

Say that you’re learning about lists and run the following code:

>>> list = [1, 2, 3, 4]
>>> list
[1, 2, 3, 4]

In this example, you’ve used list as the name for a list object containing some numbers. Reassigning a built-in name, as you did in this code snippet, shadows the original object behind the name, which prevents you from using it in your code:

>>> list(range(10))
Traceback (most recent call last):
TypeError: 'list' object is not callable

Now calling list() fails because you’ve overridden the name in your previous code. A quick fix to this issue is to use the del statement to remove the fake built-in name and recover the original name:

>>> del list  # Remove the redefined name
>>> list()  # Now the original name is available again

If you accidentally reassign a built-in name in an interactive session, then you can run a quick del name statement to remove the redefinition from your scope and restore the original built-in name in your working scope.

Removing Items From Mutable Collections

Removing items from mutable collections like lists or dictionaries is arguably the most common use case of Python’s del statement. Even though these built-in data types provide methods that you can use to remove items by index or key, del can produce slightly different results or be appropriate in different scenarios.

You can also use the del statement to remove several items from a list in one go. To do this, you can use the slice syntax. For this specific use case, you won’t find a method that performs the task. So, del is your way to go.

In the following sections, you’ll learn how to use del to remove items from existing lists. You’ll also learn how to remove key-value pairs from dictionaries using del. With these skills, you’ll be able to make your code more efficient by letting Python’s garbage collection system free up the memory resources that your lists and dictionaries won’t need any longer.

Deleting Items From a List

You’ve already learned that the list-indexing syntax, a_list[index], allows you to access individual items in a list. This syntax provides an identifier that holds a reference to the item at the target index.

If you need to delete an item from an existing list, then you can use the del statement along with the indexing operator. The general syntax is:

del a_list[index]

This statement will remove the item that lives at the position defined by index in a_list. Here’s an example so you can experience how this works in practice:

>>> computer_parts = [
...     "CPU",
...     "motherboard",
...     "case",
...     "monitor",
...     "keyboard",
...     "mouse"
... ]

>>> del computer_parts[3]
>>> computer_parts
['CPU', 'motherboard', 'case', 'keyboard', 'mouse']

>>> del computer_parts[-1]
>>> computer_parts
['CPU', 'motherboard', 'case', 'keyboard']

In this code snippet, you create a list containing some computer parts. Then you use del to remove "monitor", the item at index 3. Finally, you remove the last item in the list, using the negative index -1.

Be careful when using the extended syntax of del to remove multiple items from a list. You may end up removing the wrong items or even getting an IndexError.

For example, say that you need to remove the items containing None in a sample of numeric data so that you can use the data in some computations. Then you may think of using the following del statement:

>>> sample = [3, None, 2, 4, None, 5, 2]

>>> del sample[1], sample[4]
>>> sample
[3, 2, 4, None, 2]

What just happened? You didn’t remove the second None but instead removed the number 5. The problem is that del acts on its arguments sequentially from left to right. In this example, del first removes the second item from sample. Then it removes the fourth item, 5, from the modified list. To get the desired result, you need to consider that the target list changes after every removal.

A possible work-around is to remove items from right to left:

>>> sample = [3, None, 2, 4, None, 5, 2]

>>> del sample[4], sample[1]
>>> sample
[3, 2, 4, 5, 2]

In this example, you’ve deleted the items in reverse order starting from the greatest index and going back to the smallest one. This technique allows you to safely remove multiple items from lists using a single del statement.

When you use a list index as an argument to del, Python falls back to calling the .__delitem__() special method, which takes care of the actual removal. Here’s an example that illustrates this behavior:

>>> class List(list):
...     def __delitem__(self, index):
...         print(
...             f"Running .__delitem__() to delete {self[index]}"
...         )
...         super().__delitem__(index)

>>> sample = List([3, None, 2, 4, None, 5, 2])
>>> del sample[1]
Running .__delitem__() to delete None

>>> del sample[3]
Running .__delitem__() to delete None

In this example, you subclass the built-in list class and override its .__delitem__() method. Inside the method, you print a message to show that the method is called automatically. Then you use super() to call the original implementation of .__delitem__() in the parent class, list.

Speaking of methods, Python lists have the .remove() and .pop() methods, which allow you to remove an item by value or index, respectively:

>>> pets = ["dog", "cat", "fish", "bird", "hamster"]

>>> pets.remove("fish")  # Equivalent to del pets[2]
>>> pets
['dog', 'cat', 'bird', 'hamster']

>>> pets.pop(3)
>>> pets.pop()
>>> pets
['dog', 'cat']

In the call to .remove(), you delete the "fish" item from your pets list. This method call is equivalent to running del pets[2].

Using del vs .remove() will depend on what you’re trying to do. If want to remove items based on their index, without knowing their content, then del is the solution because it operates on item indices. On the other hand, if you want to remove list items with known content, then .remove() can be more readable and explicit. It’ll clearly communicate that you want to delete that specific value.

Then you use .pop() to delete the item at index 3. Apart from removing the target item, .pop() also returns it. That’s why you get 'hamster' on the screen. Finally, calling .pop() without a target index removes and returns the final item from the target list.

Using del vs .pop() depends even more on your specific needs. In this case, both options operate with item indices. The distinctive feature is the return value. If you don’t care about the return value, then go for del. If you need the return value for further computations, then you must use .pop().

Deleting a Slice From a List

Removing a slice of items from an existing list is another everyday use case of the del statement. You won’t find any list method that performs a similar task, so del might be your way to go. To remove a slice from a list, you need to use the following syntax:

>>> del a_list[start:stop:step]

The slicing syntax accepts up to three colon-separated arguments: start, stop, and step. They define the index that starts the slice, the index at which the slicing must stop retrieving values, and the step between values. These three arguments are commonly known as offsets.

Here are some examples of using the above construct to remove slices from lists:

>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> del digits[3:6]
>>> digits
[0, 1, 2, 6, 7, 8, 9]

>>> del digits[:3]
>>> digits
[6, 7, 8, 9]

>>> del digits[2:]
>>> digits
[6, 7]

In the first example, you remove all the items from index 3 to 6. Note that the slicing interval is open at the stop offset, which means that the item at index 6 isn’t included in the removal. In other words, the slicing operation stops when it reaches the stop offset without including the item at that offset.

In the second example, you use no value for the start offset, which means that you want to start the deletion from the first item in the list. Finally, you don’t use a stop offset in the third example. This means that you want to remove all the items from 2 to the end of the list.

You can also use the step offset to define how many items you want to jump through during the slicing. Consider the following example:

>>> numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> del numbers[::2]
>>> numbers
[2, 4, 6, 8]

In this example, you use a step of 2 to delete every other item from numbers. This is a neat trick that removes the odd numbers from your list.

You can use a slice object instead of the slicing operator to delete items from your lists:

>>> numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> every_other = slice(0, None, 2)

>>> del numbers[every_other]
>>> numbers
[2, 4, 6, 8]

In this example, you use the built-in slice() class to create a slice object. The class constructor takes the three arguments start, stop, and step. They have the same meaning as the equivalent offsets in the slicing operator.

This specific slice object retrieves items starting from 0 up to the end of the list, which you accomplish by setting the stop argument to None. The step argument takes a value of 2. This slice removes every other item from the target list.

Interestingly enough, deleting a slice of items from an existing list has the same effect as assigning an empty list to the same slice:

>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> digits[3:6] = []
>>> digits
[0, 1, 2, 6, 7, 8, 9]

>>> digits[:3] = []
>>> digits
[6, 7, 8, 9]

>>> digits[2:] = []
>>> digits
[6, 7]

Assigning an empty list to a given slice has the same effect as removing the target slice using a del statement. This example is equivalent to its first version at the beginning of this section.

Removing unnecessary items from your lists may save some memory in your programs. You typically don’t keep additional references to list items, so if you remove several items from a list, then the objects stored in those items become available for garbage collection, which may lead to releasing some memory.

Removing Keys From Dictionaries

Removing key-value pairs from a dictionary is another common use case of del. For this to work, you need to specify the key that you want to remove as an argument to del. Here’s the general syntax for that:

del a_dict[key]

This statement removes key, and its associated value, from the containing dictionary, a_dict. To see this construct in action, go ahead and run the following example:

>>> vegetable_prices = {
...     "carrots": 0.99,
...     "spinach": 1.50,
...     "onions": 0.50,
...     "cucumbers": 0.75,
...     "peppers": 1.25,
... }

>>> del vegetable_prices["spinach"]
>>> del vegetable_prices["onions"], vegetable_prices["cucumbers"]

>>> vegetable_prices
{'carrots': 0.99, 'peppers': 1.25}

Here you use del to remove the "spinach", "onions", and "cucumbers" keys from your vegetable_prices dictionary. In the case of dictionaries, it’s completely safe to remove multiple keys in a single line using the extended syntax of del because keys are unique.

You can also use some methods to remove keys from a dictionary. For example, you can use the .pop() and .popitem() methods:

>>> school_supplies = {
...     "pencil": 0.99,
...     "pen": 1.49,
...     "notebook": 2.99,
...     "highlighter": 0.79,
...     "ruler": 1.29,
... }

>>> school_supplies.pop("notebook")

>>> school_supplies.popitem()
('ruler', 1.29)
>>> school_supplies.popitem()
('highlighter', 0.79)

>>> school_supplies
{'pencil': 0.99, 'pen': 1.49}

In the first example, you use .pop() to remove the "notebook" key and its associated value, 2.99, from your school_supplies dictionary. You must call .pop() with an existing key as an argument. In the second example, you call .popitem() to delete the most recently added item from school_supplies.

Even though these methods remove keys from existing dictionaries, they work slightly differently from del. Besides removing the target key, .pop() returns the value associated with that key. Similarly, .popitem() removes the key and returns the corresponding key-value pair as a tuple. The del statement doesn’t return the removed value.

Python automatically calls the .__delitem__() special method when you use del to remove a key from a dictionary. Here’s an example that illustrates this behavior:

>>> class Dict(dict):
...     def __delitem__(self, key) -> None:
...         print(f"Running .__delitem__() to delete {(key, self[key])}")
...         super().__delitem__(key)

>>> ordinals = Dict(
...     {"First": "I", "Second": "II", "Third": "III", "Fourth": "IV"}
... )

>>> del ordinals["Second"]
Running .__delitem__() to delete ('Second', 'II')

>>> del ordinals["Fourth"]
Running .__delitem__() to delete ('Fourth', 'IV')

In this example, you override the .__delitem__() method in the Dict class, which inherits from the built-in dict class. Running the del statement with a key of Dict as an argument triggers your custom .__delitem__() implementation, which prints a message that provides information about the removed item.

To learn more about how to safely subclass the built-in dict type in Python, check out Custom Python Dictionaries: Inheriting From dict vs UserDict.

Removing Items From Mutable Nested Collections

Sometimes, you may need to nest mutable types, such as lists and dictionaries, in outer lists, tuples, and dictionaries. Once you’ve created one of these nested structures, you can access individual items on different levels of nesting using the indices or keys of those items. The leftmost indices or keys will retrieve the outer sequences or dictionaries, while the rightmost indices will retrieve the most deeply nested objects.

The possibility of accessing nested items allows you to use del to remove them using the following syntax:

# Syntax for sequences
del sequence[outer_index][nested_index_1]...[nested_index_n]

# Syntax for dictionaries
del dictionary[outer_key][nested_key_1]...[nested_key_n]

# Syntax for combined structures
del sequence_of_dicts[sequence_index][dict_key]
del dict_of_lists[dict_key][list_index]

To access and delete objects from nested mutable collections, you must use the indexing operator, providing appropriate indices from left to right. Count how many levels of nesting you’ll need to walk through to reach the target item. That’s how many indices you must provide.

Here’s an example of how to remove nested items from a list of lists:

>>> matrix = [
...     [1, 2, 3],
...     [4, 5, 6],
...     [7, 8, 9]
... ]

>>> target_row = 0
>>> target_col = 2
>>> del matrix[target_row][target_col]
>>> matrix
    [1, 2],
    [4, 5, 6],
    [7, 8, 9]

>>> target_row = 1
>>> del matrix[target_row][target_col]
>>> matrix
    [1, 2],
    [4, 5],
    [7, 8, 9]

>>> target_row = 2
>>> del matrix[target_row][target_col]
>>> matrix
    [1, 2],
    [4, 5],
    [7, 8]

In this example, you’ve used del to remove nested items in your matrix list. Specifically, you’ve removed the last value from every matrix row. Using descriptive names for the indices allows you to improve the readability of your code, making it easier to reason about.

As you can see, target_row represents the index of a nested list in matrix, while target_col represents the index of the target item in the nested list. That’s how you access nested items.

Even though Python tuples are immutable data types that don’t allow direct deletions, they can store lists, dictionaries, and other mutable types that do allow deletions. So, you can also use del on nested mutable objects in your tuples.

To illustrate this behavior, say you’re working with colors in the RGB color model. To represent each color, you use a tuple because colors should be constant. You decide to create your color objects by providing a name in uppercase and a list of RGB values:

>>> red = ("RED", [255, 0, 0])
>>> blue = ("BLUE", [0, 0, 255])

These colors look great. They have a name and a list of RBG values. You can’t delete first-level items in these tuples because tuples are immutable. However, you can delete items from the nested lists holding the RGB values:

>>> del red[1][0]
>>> red
('RED', [0, 0])

Tuples are completely immutable when all their items are immutable too. If one or more items in a tuple are mutable, then you can delete values from them using nested indexing, as you did in the above example.

Another common practice in Python is to nest lists in dictionaries. This type of construct appears often when you’re working with JSON files, which provide a clear and readable format for exchanging information.

Consider the following example of a dictionary that stores products by class:

>>> products = {
...     "fruits": ["apple", "banana", "orange"],
...     "veggies": ["tomato", "avocado", "pepper"]
... }

>>> del products["fruits"][1]
>>> products
    'fruits': ['apple', 'orange'],
    'veggies': ['tomato', 'avocado', 'pepper']

>>> del products["veggies"][0]
>>> products
    'fruits': ['apple', 'orange'],
    'veggies': ['avocado', 'pepper']

To remove an item from the inner lists in your product dictionary, you first use the dictionary key and then the index of the target item in the nested list.

You can store objects of any data type in the values of a Python dictionary. You can store strings, tuples, lists, other dictionaries, classes, functions, and even user-defined objects. If the object in a given value is mutable, then you can remove items from it using del.

Here’s another example. This time, you have a list of dictionaries. Each nested dictionary contains another dictionary:

>>> prices = [
...     {"fruits": {"apple": 1.2, "banana": 1.3, "orange": 1.5}},
...     {"veggies": {"tomato": 2.1, "avocado": 3.2, "pepper": 1.8}},
... ]

>>> del prices[0]["fruits"]["banana"]

>>> prices
    {'fruits': {'apple': 1.2, 'orange': 1.5}},
    {'veggies': {'tomato': 2.1, 'avocado': 3.2, 'pepper': 1.8}}

>>> del prices[0]["fruits"]
>>> prices
[{}, {'veggies': {'tomato': 2.1, 'avocado': 3.2, 'pepper': 1.8}}]

To remove an item from the most deeply nested dictionary, you need to use the index of the containing list, then the key of the second-level dictionary, and finally, the key of the item that you want to remove. Note that you can use other index-key combinations to delete other items, depending on their nesting level.

Deleting Members From Custom Classes

When it comes to user-defined classes, you can remove both class and instance attributes using the del statement. You can also use del to remove methods from your classes. Deleting members of classes and instances might be only rarely useful, but it’s possible in Python.

The general syntax for removing class members with del is as follows:

del a_class.class_attribute
del a_class.method

del an_instance.instance_attribute

To pass the target member as an argument to del, you need to use dot notation on either the class or one of its instances, depending on what type of member you need to remove. In the following sections, you’ll code examples of how all this works in practice.

Removing Class Members: A Generic Example

To illustrate how the del syntax for removing class members works, go ahead and write the following SampleClass class in your Python interactive REPL:

>>> class SampleClass:
...     class_attribute = 0
...     def __init__(self, arg):
...         self.instance_attribute = arg
...     def method(self):
...         print(self.instance_attribute)

>>> SampleClass.__dict__
    '__module__': '__main__',
    'class_attribute': 0,
    '__init__': <function SampleClass.__init__ at 0x105534360>,
    'method': <function SampleClass.method at 0x104bcf240>,

>>> sample_instance = SampleClass("Value")
>>> sample_instance.__dict__
{'instance_attribute': 'Value'}

In this code snippet, you create a sample class with a class attribute, an instance attribute, and a method. The highlighted lines show how these members live in the class and instance .__dict__ attributes, which store the class and instance attributes, respectively.

Now go ahead and run the following statements to delete the members of this class and its object:

>>> # Delete members through the instance
>>> del sample_instance.instance_attribute

>>> del sample_instance.class_attribute
Traceback (most recent call last):
AttributeError: 'SampleClass' object has no attribute 'class_attribute'
>>> del sample_instance.method
Traceback (most recent call last):
AttributeError: 'SampleClass' object has no attribute 'method'

>>> sample_instance.__dict__

>>> # Delete members through the class
>>> del SampleClass.class_attribute
>>> del SampleClass.method

>>> SampleClass.__dict__
    '__module__': '__main__',
    '__init__': <function SampleClass.__init__ at 0x105534360>,

Using an instance of a given class, you can only remove instance attributes. If you try to remove class attributes and methods, then you get an AttributeError exception. You can only remove class attributes and methods through the class itself, as you can confirm in the final two examples.

Removing an Instance Attribute: A Practical Example

When would del be useful for removing instance attributes? When you need to reduce your object’s memory footprint by preparing temporary instance attributes for Python’s garbage collection system, which can result in freeing up valuable memory resources on your computer.

For example, say that you want to write a Factorial class to represent the factorial of a number. The class constructor must take a number as an argument and create an instance with two read-only attributes, .number and .factorial.

The class should use caching as an optimization to avoid any unnecessary repetition of intermediate computations. It should also be memory-efficient and remove the cache after computing the factorial.

Here’s a possible implementation for your Factorial class:

 3class Factorial:
 4    def __init__(self, number):
 5        self._number = number
 6        self._cache = {0: 1, 1: 1}
 7        self._factorial = self._calculate_factorial(number)
 8        del self._cache
10    def _calculate_factorial(self, number):
11        if number in self._cache:
12            return self._cache[number]
13        current_factorial = number * self._calculate_factorial(number - 1)
14        self._cache[number] = current_factorial
15        return current_factorial
17    @property
18    def number(self):
19        return self._number
21    @property
22    def factorial(self):
23        return self._factorial
25    def __str__(self) -> str:
26        return f"{self._number}! = {self._factorial}"
28    def __repr__(self):
29        return f"{type(self).__name__}({self._number})"

Wow! There’s a lot happening in this code. Here’s a breakdown of the most relevant lines:

  • Line 4 defines the initializer, which takes your input number as an argument.

  • Line 5 defines a non-public instance attribute called ._number to store your input number.

  • Line 6 defines another instance attribute called ._cache that’ll work as a cache for intermediate computation. Initially, this attribute holds the factorial of 0 and 1.

  • Line 7 defines the ._factorial attribute to hold the factorial of the input number. Note that this attribute gets its value from a call to ._calculate_factorial().

  • Line 8 uses del to remove the cached data. This removal will allow you to free up memory for later use and make your Factorial instances more lightweight and your code more memory-efficient.

  • Lines 10 to 15 define the ._calculate_factorial() method, which calculates the factorial of your input number, using ._cache to store the already-computed values.

  • Lines 17 to 23 define two properties to manage the .number and .factorial attributes. Note that these properties don’t have setter methods, which makes the underlying attributes read-only.

  • Lines 25 to 26 define a .__str__() method that returns a user-friendly string representation for your instances.

  • Lines 28 to 29 define a .__repr__() method that returns a developer-friendly string representation of Factorial objects.

Here’s how your class works in practice:

>>> from factorial import Factorial

>>> for i in range(5):
...     print(Factorial(i))
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24

>>> factorial_5 = Factorial(5)
>>> factorial_5
>>> factorial_5.number
>>> factorial_5.factorial
>>> print(factorial_5)
5! = 120

>>> factorial_5.factorial = 720
Traceback (most recent call last):
AttributeError: property 'factorial' of 'Factorial' object has no setter

>>> factorial_5._cache
Traceback (most recent call last):
AttributeError: 'Factorial' object has no attribute '_cache'

In the first example, you run a for loop that creates five instances of Factorial and prints them to the screen. Note how the .__str__() method works by printing the instance using the mathematical factorial notation.

Then you create another instance of Factorial with 5 as the input number. Note how you can access the .number and .factorial attributes but can’t reassign them because they’re read-only. Finally, if you try to access the non-public ._cache attribute, then you get an AttributeError because you already removed this attribute in the .__init__() method.

Deleting the ._cache attribute after the factorial computation makes the instances of Factorial memory-efficient. To illustrate this, go ahead and add the Pympler library by installing pympler with pip. This library will allow you to measure the memory footprint of your Factorial instances.

Then execute the following code:

>>> from pympler import asizeof

>>> asizeof.asizeof(Factorial(100))

Here, you use the asizeof() function to calculate the combined size in bytes of a Factorial instance. As you can see, this specific instance of Factorial occupies 600 bytes in your computer’s memory.

Now get back to your file and comment out line 8, which contains the del statement that removes ._cache. With this change in place, go ahead and run the above function call again:

>>> asizeof.asizeof(Factorial(100))

Now the size of your Factorial instance is more than 23 times greater than before. That’s the impact of removing vs keeping the unnecessary cached data once the instance is complete.

Preventing Attribute Deletion in Custom Classes

In Python, you can write your classes in a way that prevents the removal of instance attributes. You’ll find a bunch of techniques to do this. In this section, you’ll learn about two of these techniques:

  1. Providing a custom .__delattr__() method
  2. Using a property with a deleter method

To kick things off, you’ll start by overriding the .__delattr__() special method with a custom implementation. Under the hood, Python automatically calls this method when you use a given instance attribute as an argument to the del statement.

As an example of how to create a class that prevents you from removing its instance attribute, consider the following toy class:


class NonDeletable:
    def __init__(self, value):
        self.value = value

    def __delattr__(self, name):
        raise AttributeError(
            f"{type(self).__name__} object doesn't support attribute deletion"

In this class, you override the .__delattr__() special method. Your implementation raises an AttributeError exception whenever a user tries to remove any attribute, like the .value, using a del statement.

Here’s an example of how your class behaves in practice:

>>> one = NonDeletable(1)
>>> one.value

>>> del one.value
Traceback (most recent call last):
AttributeError: NonDeletable object doesn't support attribute deletion

Whenever you try to remove the .value attribute of any instance of NonDeletable, you get an AttributeError exception that comes from your custom implementation of .__delattr__(). This technique provides a quick solution for those situations where you need to prevent instance attribute deletion.

Another common, quick technique for preventing instance attribute deletions is to turn your target attribute into a property and provide a suitable deleter method. For example, say that you want to code a Person class that prevents users from deleting its .name attribute:


class Person:
    def __init__(self, name): = name

    def name(self):
        return self._name

    def name(self, value):
        self._name = str(value).upper()

    def name(self):
        raise AttributeError("can't delete attribute 'name'")

In this example, Person has a .name attribute that you implement through a property. The attribute has a getter method that returns the value of ._name, which is the regular attribute that stores the data.

The setter method takes a new value for the attributes, converts it into a string, and transforms it into uppercase letters.

Finally, the deleter method raises an AttributeError to signal that the attribute isn’t deletable. Python calls this method automatically when you use the .name attribute as an argument in a del statement:

>>> from person import Person

>>> jane = Person("Jane")
>>> = "Jane Doe"

>>> del
Traceback (most recent call last):
AttributeError: can't delete attribute 'name'

Your Person class works as expected. You can access its .name attribute and update it with new values while uppercasing the input value every time. The deleter method of your .name property at the end of the class definition ensures that your users can’t delete the attribute.


You’ve learned how Python’s del statement works and how to use it for deleting names and references in your code. You now know how to remove variables from different namespaces, items from lists, keys from dictionaries, and members from custom objects and classes. You’ve also learned how del can assist you when you need to free up memory to make your code more efficient and lightweight.

In this tutorial, you’ve learned how to:

  • Write del statements in your Python code
  • Remove names from different namespaces using del
  • Quickly delete list items and dictionary keys with del
  • Use del to remove attributes for user-defined classes and objects
  • Prevent attribute deletion in your custom classes

Now that you’ve learned a lot about Python’s del statement, you can write memory-efficient code by removing references to unneeded objects, which prepares those objects for the garbage collection system.

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Leodanis Pozo Ramos

Leodanis Pozo Ramos Leodanis Pozo Ramos

Leodanis is an industrial engineer who loves Python and software development. He's a self-taught Python developer with 6+ years of experience. He's an avid technical writer with a growing number of articles published on Real Python and other sites.

» More about Leodanis

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:
Tweet Share Share Email

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.

Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!

Keep Learning

Related Tutorial Categories: basics best-practices python