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.
Free Bonus: Click here to download sample code that’ll empower you to write more efficient programs with Python’s del
statement.
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.
Note: In this tutorial, you’ll use the term reference to generically designate names or identifiers that can hold references to objects in Python.
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.
Note: The del
statement isn’t the only tool that you can use to lower the reference count of a given object. If you make an existing reference point to another object, then you implicitly remove one reference to the original object.
Another action that lowers the reference count is making a given reference go out of scope, implicitly removing the reference.
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
314
>>> number = None
>>> print(number)
None
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.
Note: You can inspect all the names that live in your global and built-in scopes using the built-in globals()
function. Similarly, you can access the names in your current local scope using the locals()
function. Both functions return dictionary objects mapping names to objects in their target scopes.
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])
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
.
Note: The del
statement fails to remove items from immutable sequence types, such as strings, bytes, and tuples:
>>> site = "realpython.com/"
>>> del site[-1]
Traceback (most recent call last):
...
TypeError: 'str' object doesn't support item deletion
>>> color = (255, 0, 0)
>>> del color[0]
Traceback (most recent call last):
...
TypeError: 'tuple' object doesn't support item deletion
Deleting an item from this tuple would imply an in-place mutation, which isn’t allowed in immutable collection types.
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
.
Note: To learn more about subclassing the built-in list
type, check out Custom Python Lists: Inheriting From list
vs UserList
.
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)
'hamster'
>>> pets.pop()
'bird'
>>> 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.
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 stop
, 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
index without including the item at that index.
In the second example, you use no value for start
, which means that you want to start the deletion from the first item in the list. Finally, you don’t use a stop
value 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 step
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 indices 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")
2.99
>>> 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__
mappingproxy({
'__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__
mappingproxy({
'__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:
1# factorial.py
2
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
9
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
16
17 @property
18 def number(self):
19 return self._number
20
21 @property
22 def factorial(self):
23 return self._factorial
24
25 def __str__(self) -> str:
26 return f"{self._number}! = {self._factorial}"
27
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 of0
and1
. -
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 yourFactorial
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 ofFactorial
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)
>>> factorial_5.number
5
>>> factorial_5.factorial
120
>>> 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))
600
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 factorial.py
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))
14216
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:
- Providing a custom
.__delattr__()
method - 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:
# non_deletable.py
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
1
>>> 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:
# person.py
class Person:
def __init__(self, name):
self.name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = str(value).upper()
@name.deleter
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.name
'JANE'
>>> jane.name = "Jane Doe"
>>> jane.name
'JANE DOE'
>>> del jane.name
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.
Conclusion
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.
Free Bonus: Click here to download sample code that’ll empower you to write more efficient programs with Python’s del
statement.