The list class is a fundamental built-in data type in Python. It has an impressive and useful set of features, allowing you to efficiently organize and manipulate heterogeneous data. Knowing how to use lists is a must-have skill for you as a Python developer. Lists have many use cases, so you’ll frequently reach for them in real-world coding.
By working through this tutorial, you’ll dive deep into lists and get a solid understanding of their key features. This knowledge will allow you to write more effective code by taking advantage of lists.
In this tutorial, you’ll learn how to:
- Create new lists in Python
- Access the items in an existing list
- Copy, update, grow, shrink, and concatenate existing lists
- Sort, reverse, and traverse existing lists
- Use other features of Python lists
In addition, you’ll code some examples that showcase common use cases of lists in Python. They’ll help you understand how to better use lists in your code.
To get the most out of this tutorial, you should have a good understanding of core Python concepts, including variables, functions, and for loops. You’ll also benefit from familiarity with other built-in data types, such as strings, tuples, dictionaries, and sets.
Free Bonus: Click here to download the sample code that will make you an expert on Python’s list data type.
Getting Started With Python’s list Data Type
Python’s list is a flexible, versatile, powerful, and popular built-in data type. It allows you to create variable-length and mutable sequences of objects. In a list, you can store objects of any type. You can also mix objects of different types within the same list, although list elements often share the same type.
Note: Throughout this tutorial, you’ll use the terms items, elements, and values interchangeably to refer to the objects stored in a list.
Some of the more relevant characteristics of list objects include being:
- Ordered: They contain elements or items that are sequentially arranged according to their specific insertion order.
- Zero-based: They allow you to access their elements by indices that start from zero.
- Mutable: They support in-place mutations or changes to their contained elements.
- Heterogeneous: They can store objects of different types.
- Growable and dynamic: They can grow or shrink dynamically, which means that they support the addition, insertion, and removal of elements.
- Nestable: They can contain other lists, so you can have lists of lists.
- Iterable: They support iteration, so you can traverse them using a loop or comprehension while you perform operations on each of their elements.
- Sliceable: They support slicing operations, meaning that you can extract a series of elements from them.
- Combinable: They support concatenation operations, so you can combine two or more lists using the concatenation operators.
- Copyable: They allow you to make copies of their content using various techniques.
Lists are sequences of objects. They’re commonly called containers or collections because a single list can contain or collect an arbitrary number of other objects.
Note: In Python, lists support a rich set of operations that are common to all sequence types, including tuples, strings, and ranges. These operations are known as common sequence operations. Throughout this tutorial, you’ll learn about several operations that fall into this category.
In Python, lists are ordered, which means that they keep their elements in the order of insertion:
>>> colors = [
... "red",
... "orange",
... "yellow",
... "green",
... "blue",
... "indigo",
... "violet"
... ]
>>> colors
['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']
The items in this list are strings representing colors. If you access the list object, then you’ll see that the colors keep the same order in which you inserted them into the list. This order remains unchanged during the list’s lifetime unless you perform some mutations on it.
You can access an individual object in a list by its position or index in the sequence. Indices start from zero:
>>> colors[0]
'red'
>>> colors[1]
'orange'
>>> colors[2]
'yellow'
>>> colors[3]
'green'
Positions are numbered from zero to the length of the list minus one. The element at index 0 is the first element in the list, the element at index 1 is the second, and so on.
Lists can contain objects of different types. That’s why lists are heterogeneous collections:
[42, "apple", True, {"name": "John Doe"}, (1, 2, 3), [3.14, 2.78]]
This list contains objects of different data types, including an integer number, string, Boolean value, dictionary, tuple, and another list. Even though this feature of lists may seem cool, in practice you’ll find that lists typically store homogeneous data.
Note: One of the most relevant characteristics of lists is that they’re mutable data types. This feature deeply impacts their behavior and use cases. For example, mutability implies that lists aren’t hashable, so you can’t use them as dictionary keys. You’ll learn a lot about how mutability affects lists throughout this tutorial. So, keep reading!
Okay! That’s enough for a first glance at Python lists. In the rest of this tutorial, you’ll dive deeper into all the above characteristics of lists and more. Are you ready? To kick things off, you’ll start by learning the different ways to create lists.
Constructing Lists in Python
First things first. If you want to use a list to store or collect some data in your code, then you need to create a list object. You’ll find several ways to create lists in Python. That’s one of the features that make lists so versatile and popular.
For example, you can create lists using one of the following tools:
- List literals
- The
list()constructor - A list comprehension
In the following sections, you’ll learn how to use the three tools listed above to create new lists in your code. You’ll start off with list literals.
Creating Lists Through Literals
List literals are probably the most popular way to create a list object in Python. These literals are fairly straightforward. They consist of a pair of square brackets enclosing a comma-separated series of objects.
Here’s the general syntax of a list literal:
[item_0, item_1, ..., item_n]
This syntax creates a list of n items by listing the items in an enclosing pair of square brackets. Note that you don’t have to declare the items’ type or the list’s size beforehand. Remember that lists have a variable size and can store heterogeneous objects.
Here are a few examples of how to use the literal syntax to create new lists:
>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> fruits = ["apple", "banana", "orange", "kiwi", "grape"]
>>> cities = [
... "New York",
... "Los Angeles",
... "Chicago",
... "Houston",
... "Philadelphia"
... ]
>>> matrix = [
... [1, 2, 3],
... [4, 5, 6],
... [7, 8, 9]
... ]
>>> inventory = [
... {"product": "phone", "price": 1000, "quantity": 10},
... {"product": "laptop", "price": 1500, "quantity": 5},
... {"product": "tablet", "price": 500, "quantity": 20}
... ]
>>> functions = [print, len, range, type, enumerate]
>>> empty = []
In these examples, you use the list literal syntax to create lists containing numbers, strings, other lists, dictionaries, and even function objects. As you already know, lists can store any type of object. They can also be empty, like the final list in the above code snippet.
Empty lists are useful in many situations. For example, maybe you want to create a list of objects resulting from computations that run in a loop. The loop will allow you to populate the empty list one element at a time.
Using a list literal is arguably the most common way to create lists. You’ll find these literals in many Python examples and codebases. They come in handy when you have a series of elements with closely related meanings, and you want to pack them into a single data structure.
Note that naming lists as plural nouns is a common practice that improves readability. However, there are situations where you can use collective nouns as well.
For example, you can have a list called people. In this case, every item will be a person. Another example would be a list that represents a table in a database. You can call the list table, and each item will be a row. You’ll find more examples like these in your walk-through of using lists.
Using the list() Constructor
Another tool that allows you to create list objects is the class constructor, list(). You can call this constructor with any iterable object, including other lists, tuples, sets, dictionaries and their components, strings, and many others. You can also call it without any arguments, in which case you’ll get an empty list back.
Here’s the general syntax:
list([iterable])
To create a list, you need to call list() as you’d call any class constructor or function. Note that the square brackets around iterable mean that the argument is optional, so the brackets aren’t part of the syntax. Here are a few examples of how to use the constructor:
>>> list((0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list({"circle", "square", "triangle", "rectangle", "pentagon"})
['square', 'rectangle', 'triangle', 'pentagon', 'circle']
>>> list({"name": "John", "age": 30, "city": "New York"}.items())
[('name', 'John'), ('age', 30), ('city', 'New York')]
>>> list("Pythonista")
['P', 'y', 't', 'h', 'o', 'n', 'i', 's', 't', 'a']
>>> list()
[]
In these examples, you create different lists using the list() constructor, which accepts any type of iterable object, including tuples, dictionaries, strings, and many more. It even accepts sets, in which case you need to remember that sets are unordered data structures, so you won’t be able to predict the final order of items in the resulting list.
Calling list() without an argument creates and returns a new empty list. This way of creating empty lists is less common than using an empty pair of square brackets. However, in some situations, it can make your code more explicit by clearly communicating your intent: creating an empty list.
The list() constructor is especially useful when you need to create a list out of an iterator object. For example, say that you have a generator function that yields numbers from the Fibonacci sequence on demand, and you need to store the first ten numbers in a list.
In this case, you can use list() as in the code below:
>>> def fibonacci_generator(stop):
... current_fib, next_fib = 0, 1
... for _ in range(0, stop):
... fib_number = current_fib
... current_fib, next_fib = next_fib, current_fib + next_fib
... yield fib_number
...
>>> fibonacci_generator(10)
<generator object fibonacci_generator at 0x10692f3d0>
>>> list(fibonacci_generator(10))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Calling fibonacci_generator() directly returns a generator iterator object that allows you to iterate over the numbers in the Fibonacci sequence up to the index of your choice. However, you don’t need an iterator in your code. You need a list. A quick way to get that list is to wrap the iterator in a call to list(), as you did in the final example.
This technique comes in handy when you’re working with functions that return iterators, and you want to construct a list object out of the items that the iterator yields. The list() constructor will consume the iterator, build your list, and return it back to you.
Note: You can also use the literal syntax and the iterable unpacking operator (*) as an alternative to the list() constructor.
Here’s how:
>>> [*fibonacci_generator(10)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
In this example, the iterable unpacking operator consumes the iterator, and the square brackets build the final list of numbers. However, this technique is less readable and explicit than using list().
As a side note, you’ll often find that built-in and third-party functions return iterators. Functions like reversed(), enumerate(), map(), and filter() are good examples of this practice. It’s less common to find functions that directly return list objects, but the built-in sorted() function is one example. It takes an iterable as an argument and returns a list of sorted items.
Building Lists With List Comprehensions
List comprehensions are one of the most distinctive features of Python. They’re quite popular in the Python community, so you’ll likely find them all around. List comprehensions allow you to quickly create and transform lists using a syntax that mimics a for loop but in a single line of code.
The core syntax of list comprehensions looks something like this:
[expression(item) for item in iterable]
Every list comprehension needs at least three components:
expression()is a Python expression that returns a concrete value, and most of the time, that value depends onitem. Note that it doesn’t have to be a function.itemis the current object fromiterable.iterablecan be any Python iterable object, such as a list, tuple, set, string, or generator.
The for construct iterates over the items in iterable, while expression(item) provides the corresponding list item that results from running the comprehension.
To illustrate how list comprehensions allow you to create new lists out of existing iterables, say that you want to construct a list with the square values of the first ten integer numbers. In this case, you can write the following comprehension:
>>> [number ** 2 for number in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
In this example, you use range() to get the first ten integer numbers. The comprehension iterates over them while computing the square and building the new list. This example is just a quick sample of what you can do with a list comprehension.
Note: To dive deeper into list comprehensions and how to use them, check out When to Use a List Comprehension in Python.
In general, you’ll use a list comprehension when you need to create a list of transformed values out of an existing iterable. Comprehensions are a great tool that you need to master as a Python developer. They’re optimized for performance and are quick to write.
Accessing Items in a List: Indexing
You can access individual items from a list using the item’s associated index. What’s an item’s index? Each item in a list has an index that specifies its position in the list. Indices are integer numbers that start at 0 and go up to the number of items in the list minus 1.
To access a list item through its index, you use the following syntax:
list_object[index]
This construct is known as an indexing operation, and the [index] part is known as the indexing operator. It consists of a pair of square brackets enclosing the desired or target index. You can read this construct as from list_object give me the item at index.
Here’s how this syntax works in practice:
>>> languages = ["Python", "Java", "JavaScript", "C++", "Go", "Rust"]
>>> languages[0]
'Python'
>>> languages[1]
'Java'
>>> languages[2]
'JavaScript'
>>> languages[3]
'C++'
>>> languages[4]
'Go'
>>> languages[5]
'Rust'
Indexing your list with different indices gives you direct access to the underlying items. If you use the Big O notation for time complexity, then you can say that indexing is an O(1) operation. This means that lists are quite good for those situations where you need to access random items from a series of items.
Here’s a more visual representation of how indices map to items in a list:
"Python" |
"Java" |
"JavaScript" |
"C++" |
"Go" |
"Rust" |
|---|---|---|---|---|---|
0 |
1 |
2 |
3 |
4 |
5 |
In any Python list, the index of the first item is 0, the index of the second item is 1, and so on. The index of the last item is the number of items minus 1.
The number of items in a list is known as the list’s length. You can check the length of a list by using the built-in len() function:
>>> len(languages)
6
So, the index of the last item in languages is 6 - 1 = 5. That’s the index of the "Rust" item in your sample list. If you use an index greater than this number in an indexing operation, then you get an IndexError exception:
>>> languages[6]
Traceback (most recent call last):
...
IndexError: list index out of range
In this example, you try to retrieve the item at index 6. Because this index is greater than 5, you get an IndexError as a result. Using out-of-range indices can be an incredibly common issue when you work with lists, so keep an eye on your target indices.
Indexing operations are quite flexible in Python. For example, you can also use negative indices while indexing lists. This kind of index gives you access to the list items in backward order:
>>> languages[-1]
'Rust'
>>> languages[-2]
'Go'
>>> languages[-3]
'C++'
>>> languages[-4]
'JavaScript'
>>> languages[-5]
'Java'
>>> languages[-6]
'Python'
A negative index specifies an element’s position relative to the right end of the list, moving back to the beginning of the list. Here’s a representation of the process:
"Python" |
"Java" |
"JavaScript" |
"C++" |
"Go" |
"Rust" |
|---|---|---|---|---|---|
-6 |
-5 |
-4 |
-3 |
-2 |
-1 |
You can access the last item in a list using index -1. Similarly, index -2 specifies the next-to-last item, and so forth. It’s important to note that negative indices don’t start from 0 because 0 already points to the first item. This may be confusing when you’re first learning about negative and positive indices, but you’ll get used to it. It just takes a bit of practice.
Note that if you use negative indices, then -len(languages) will be the first item in the list. If you use an index lower than that value, then you get an IndexError:
>>> languages[-7]
Traceback (most recent call last):
...
IndexError: list index out of range
When you use an index lower than -len(languages), you get an error telling you that the target index is out of range.
Using negative indices can be very convenient in many situations. For example, accessing the last item in a list is a fairly common operation. In Python, you can do this by using negative indices, like in languages[-1], which is more readable and concise than doing something like languages[len(languages) - 1].
Note: Negative indices also come in handy when you need to reverse a list, as you’ll learn in the section Reversing and Sorting Lists.
As you already know, lists can contain items of any type, including other lists and sequences. When you have a list containing other sequences, you can access the items in any nested sequence by chaining indexing operations. Consider the following list of employee records:
>>> employees = [
... ("John", 30, "Software Engineer"),
... ("Alice", 25, "Web Developer"),
... ("Bob", 45, "Data Analyst"),
... ("Mark", 22, "Intern"),
... ("Samantha", 36, "Project Manager")
... ]
How can you access the individual pieces of data from any given employee? You can use the following indexing syntax:
list_of_sequences[index_0][index_1]...[index_n]
The number at the end of each index represents the level of nesting for the list. For example, your employee list has one level of nesting. Therefore, to access Alice’s data, you can do something like this:
>>> employees[1][0]
'Alice'
>>> employees[1][1]
25
>>> employees[1][2]
'Web Developer'
In this example, when you do employees[1][0], index 1 refers to the second item in the employees list. That’s a nested list containing three items. The 0 refers to the first item in that nested list, which is "Alice". As you can see, you can access items in the nested lists by applying multiple indexing operations in a row. This technique is extensible to lists with more than one level of nesting.
If the nested items are dictionaries, then you can access their data using keys:
>>> employees = [
... {"name": "John", "age": 30, "job": "Software Engineer"},
... {"name": "Alice", "age": 25, "job": "Web Developer"},
... {"name": "Bob", "age": 45, "job": "Data Analyst"},
... {"name": "Mark", "age": 22, "job": "Intern"},
... {"name": "Samantha", "age": 36, "job": "Project Manager"}
... ]
>>> employees[3]["name"]
'Mark'
>>> employees[3]["age"]
22
>>> employees[3]["job"]
Intern
In this example, you have a list of dictionaries. To access the data from one of the dictionaries, you need to use its index in the list, followed by the target key in square brackets.
Retrieving Multiple Items From a List: Slicing
Another common requirement when working with lists is to extract a portion, or slice, of a given list. You can do this with a slicing operation, which has the following syntax:
list_object[start:stop:step]
The [start:stop:step] part of this construct is known as the slicing operator. Its syntax consists of a pair of square brackets and three optional indices, start, stop, and step. The second colon is optional. You typically use it only in those cases where you need a step value different from 1.
Note: Slicing is an operation that’s common to all Python sequence data types, including lists, tuples, strings, ranges, and others.
Here’s what the indices in the slicing operator mean:
startspecifies the index at which you want to start the slicing. The resulting slice includes the item at this index.stopspecifies the index at which you want the slicing to stop extracting items. The resulting slice doesn’t include the item at this index.stepprovides an integer value representing how many items the slicing will skip on each step. The resulting slice won’t include the skipped items.
All the indices in the slicing operator are optional. They have the following default values:
| Index | Default Value |
|---|---|
start |
0 |
stop |
len(list_object) |
step |
1 |
The minimal working variation of the indexing operator is [:]. In this variation, you rely on the default values of all the indices and take advantage of the fact that the second colon is optional. The [::] variation of the slicing operator produces the same result as [:]. This time, you rely on the default value of the three indices.
Note: Both of the above variations of the slicing operator ([:] and [::]) allow you to create a shallow copy of your target list. You’ll learn more about this topic in the Shallow Copies of a List section.
Now it’s time for you to explore some examples of how slicing works:
>>> letters = ["A", "a", "B", "b", "C", "c", "D", "d"]
>>> upper_letters = letters[0::2] # Or [::2]
>>> upper_letters
['A', 'B', 'C', 'D']
>>> lower_letters = letters[1::2]
>>> lower_letters
['a', 'b', 'c', 'd']
In this example, you have a list of letters in uppercase and lowercase. You want to extract the uppercase letters into one list and the lowercase letters into another list. The [0::2] operator helps you with the first task, and [1::2] helps you with the second.
In both examples, you’ve set step to 2 because you want to retrieve every other letter from the original list. In the first slicing, you use a start of 0 because you want to start from the very beginning of the list. In the second slicing, you use a start of 1 because you need to jump over the first item and start extracting items from the second one.
You can use any variation of the slicing operator that fits your needs. In many situations, relying on the default indices is pretty helpful. In the examples above, you rely on the default value of stop, which is len(list_object). This practice allows you to run the slicing all the way up to the last item of the target list.
Here are a few more examples of slicing:
>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> first_three = digits[:3]
>>> first_three
[0, 1, 2]
>>> middle_four = digits[3:7]
>>> middle_four
[3, 4, 5, 6]
>>> last_three = digits[-3:]
>>> last_three
[7, 8, 9]
>>> every_other = digits[::2]
>>> every_other
[0, 2, 4, 6, 8]
>>> every_three = digits[::3]
>>> every_three
[0, 3, 6, 9]
In these examples, the variable names reflect the portion of the list that you’re extracting in every slicing operation. As you can conclude, the slicing operator is pretty flexible and versatile. It even allows you to use negative indices.
Every slicing operation uses a slice object internally. The built-in slice() function provides an alternative way to create slice objects that you can use to extract multiple items from a list. The signature of this built-in function is the following:
slice(start, stop, step)
It takes three arguments with the same meaning as the indices in the slicing operator and returns a slice object equivalent to [start:stop:step]. To illustrate how slice() works, get back to the letters example and rewrite it using this function instead of the slicing operator. You’ll end up with something like the following:
>>> letters = ["A", "a", "B", "b", "C", "c", "D", "d"]
>>> upper_letters = letters[slice(0, None, 2)]
>>> upper_letters
['A', 'B', 'C', 'D']
>>> lower_letters = letters[slice(1, None, 2)]
>>> lower_letters
['a', 'b', 'c', 'd']
Passing None to any arguments of slice() tells the function that you want to rely on its internal default value, which is the same as the equivalent index’s default in the slicing operator. In these examples, you pass None to stop, which tells slice() that you want to use len(letters) as the value for stop.
As an exercise, you can write the digits examples using slice() instead of the slicing operator. Go ahead and give it a try! This practice will help you better understand the intricacies of slicing operations in Python.
Finally, it’s important to note that out-of-range values for start and stop don’t cause slicing expressions to raise a TypeError exception. In general, you’ll observe the following behaviors:
- If
startis before the beginning of the list, which can happen when you use negative indices, then Python will use0instead. - If
startis greater thanstop, then the slicing will return an empty list. - If
stopis beyond the length of the list, then Python will use the length of the list instead.
Here are some examples that show these behaviors in action:
>>> colors = [
... "red",
... "orange",
... "yellow",
... "green",
... "blue",
... "indigo",
... "violet"
... ]
>>> len(colors)
7
>>> colors[-8:]
['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']
>>> colors[8:]
[]
>>> colors[:8]
['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']
In these examples, your color list has seven items, so len(colors) returns 7. In the first example, you use a negative value for start. The first item of colors is at index -7. Because -8 < -7, Python replaces your start value with 0, which results in a slice that contains the items from 0 to the end of the list.
Note: In the examples above, you use only one colon in each slicing. In day-to-day coding, this is common practice. You’ll only use the second colon if you need to provide a step different from 1. Here’s an example where step equals 2:
>>> colors[::2]
['red', 'yellow', 'blue', 'violet']
In this example, you set step to 2 because you need a copy of colors that contains every other color. The slicing jumps over two colors in every step and gives you back a list of four colors.
In the second example, you use a start value that’s greater than the length of colors. Because there’s nothing to retrieve beyond the end of the list, Python returns an empty list. In the final example, you use a stop value that’s greater than the length of colors. In this case, Python retrieves all the items up to the end of the list.
Creating Copies of a List
Creating copies of an existing list is a common need in Python code. Having a copy ensures that when you change a given list, that change doesn’t affect the original data or the data in other copies.
Note: In Python, an object’s identity is a unique identifier that distinguishes it from other objects. You can use the built-in id() function to get the identity of any Python object. In Python’s CPython implementation, an object’s identity coincides with the memory address where the object is stored.
In Python, you’ll have two kinds of mechanisms to create copies of an existing list. You can create either:
- A shallow copy
- A deep copy
Both types of copies have specific characteristics that will directly impact their behavior. In the following sections, you’ll learn how to create shallow and deep copies of existing lists in Python. First, you’ll take a glance at aliases, a related concept that can cause some confusion and lead to issues and bugs.
Aliases of a List
In Python, you can create aliases of variables using the assignment operator (=). Assignments don’t create copies of objects in Python. Instead, they create bindings between the variable and the object involved in the assignment. Therefore, when you have several aliases of a given list, changes in an alias will affect the rest of the aliases.
To illustrate how you can create aliases and how they work, consider the following example:
>>> countries = ["United States", "Canada", "Poland", "Germany", "Austria"]
>>> nations = countries
>>> id(countries) == id(nations)
True
>>> countries[0] = "United States of America"
>>> nations
['United States of America', 'Canada', 'Poland', 'Germany', 'Austria']
In this code snippet, the first highlighted line creates nations as an alias of countries. Note how both variables point to the same object, which you know because the object’s identity is the same. In the second highlighted line, you update the object at index 0 in countries. This change reflects in the nations alias.
Assignment statements like the one in the first highlighted line above don’t create copies of the right-hand object. They just create aliases or variables that point to the same underlying object.
In general, aliases can come in handy in situations where you need to avoid name collisions in your code or when you need to adapt the names to specific naming patterns.
To illustrate, say that you have an app that uses your list of countries as countries in one part of the code. The app requires the same list in another part of the code, but there’s already a variable called countries with other content.
If you want both pieces of code to work on the same list, then you can use nations as an alias for countries. A handy way to do this would be to use the as keyword for creating the alias through an implicit assignment, for example, when you import the list from another module.
Shallow Copies of a List
A shallow copy of an existing list is a new list containing references to the objects stored in the original list. In other words, when you create a shallow copy of a list, Python constructs a new list with a new identity. Then, it inserts references to the objects in the original list into the new list.
There are at least three different ways to create shallow copies of an existing list. You can use:
These three tools demonstrate equivalent behavior. So, to kick things off, you’ll start exploring the slicing operator:
>>> countries = ["United States", "Canada", "Poland", "Germany", "Austria"]
>>> nations = countries[:]
>>> nations
['United States', 'Canada', 'Poland', 'Germany', 'Austria']
>>> id(countries) == id(nations)
False
The highlighted line creates nations as a shallow copy of countries by using the slicing operator with one colon only. This operation takes a slice from the beginning to the end of countries. In this case, nations and countries have different identities. They’re completely independent list objects.
However, the elements in nations are aliases of the elements in countries:
>>> id(nations[0]) == id(countries[0])
True
>>> id(nations[1]) == id(countries[1])
True
As you can see, items under the same index in both nations and countries share the same object identity. This means that you don’t have copies of the items. You’re really sharing them. This behavior allows you to save some memory when working with lists and their copies.
Now, how would this impact the behavior of both lists? If you changed an item in nations, would the change reflect in countries? The code below will help you answer these questions:
>>> countries[0] = "United States of America"
>>> countries
['United States of America', 'Canada', 'Poland', 'Germany', 'Austria']
>>> nations
['United States', 'Canada', 'Poland', 'Germany', 'Austria']
>>> id(countries[0]) == id(nations[0])
False
>>> id(countries[1]) == id(nations[1])
True
On the first line of this piece of code, you update the item at index 0 in countries. This change doesn’t affect the item at index 0 in nations. Now the first items in the lists are completely different objects with their own identities. The rest of the items, however, continue to share the same identity. So, they’re the same object in each case.
Because making copies of a list is such a common operation, the list class has a dedicated method for it. The method is called .copy(), and it returns a shallow copy of the target list:
>>> countries = ["United States", "Canada", "Poland", "Germany", "Austria"]
>>> nations = countries.copy()
>>> nations
['United States', 'Canada', 'Poland', 'Germany', 'Austria']
>>> id(countries) == id(nations)
False
>>> id(countries[0]) == id(nations[0])
True
>>> id(countries[1]) == id(nations[1])
True
>>> countries[0] = "United States of America"
>>> countries
['United States of America', 'Canada', 'Poland', 'Germany', 'Austria']
>>> nations
['United States', 'Canada', 'Poland', 'Germany', 'Austria']
Calling .copy() on countries gets you a shallow copy of this list. Now you have two different lists. However, their elements are common to both. Again, if you change an element in one of the lists, then the change won’t reflect in the copy.
You’ll find yet another tool for creating shallow copies of a list. The copy() function from the copy module allows you to do just that:
>>> from copy import copy
>>> countries = ["United States", "Canada", "Poland", "Germany", "Austria"]
>>> nations = copy(countries)
>>> nations
['United States', 'Canada', 'Poland', 'Germany', 'Austria']
>>> id(countries) == id(nations)
False
>>> id(countries[0]) == id(nations[0])
True
>>> id(countries[1]) == id(nations[1])
True
>>> countries[0] = "United States of America"
>>> countries
['United States of America', 'Canada', 'Poland', 'Germany', 'Austria']
>>> nations
['United States', 'Canada', 'Poland', 'Germany', 'Austria']
When you feed copy() with a mutable container data type, such as a list, the function returns a shallow copy of the input object. This copy behaves the same as the previous shallow copies that you’ve built in this section.
Deep Copies of a List
Sometimes you may need to build a complete copy of an existing list. In other words, you want a copy that creates a new list object and also creates new copies of the contained elements. In these situations, you’ll have to construct what’s known as a deep copy.
When you create a deep copy of a list, Python constructs a new list object and then inserts copies of the objects from the original list recursively.
To create a deep copy of an existing list, you can use the deepcopy() function from the copy module. Here’s an example of how this function works:
>>> from copy import deepcopy
>>> matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> matrix_copy = deepcopy(matrix)
>>> id(matrix) == id(matrix_copy)
False
>>> id(matrix[0]) == id(matrix_copy[0])
False
>>> id(matrix[1]) == id(matrix_copy[1])
False
In this example, you create a deep copy of your matrix list. Note how both the lists and their sibling items have different identities.
Why would you need to create a deep copy of matrix, anyway? For example, if you only create a shallow copy of matrix, then you can face some issues when trying to mutate the nested lists:
>>> from copy import copy
>>> matrix_copy = copy(matrix)
>>> matrix_copy[0][0] = 100
>>> matrix_copy[0][1] = 200
>>> matrix_copy[0][2] = 300
>>> matrix_copy
[[100, 200, 300], [4, 5, 6], [7, 8, 9]]
>>> matrix
[[100, 200, 300], [4, 5, 6], [7, 8, 9]]
In this example, you create a shallow copy of matrix. If you change items in a nested list within matrix_copy, then those changes affect the original data in matrix. The way to avoid this behavior is to use a deep copy:
>>> matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> matrix_copy = deepcopy(matrix)
>>> matrix_copy[0][0] = 100
>>> matrix_copy[0][1] = 200
>>> matrix_copy[0][2] = 300
>>> matrix_copy
[[100, 200, 300], [4, 5, 6], [7, 8, 9]]
>>> matrix
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Now the changes in matrix_copy or any other deep copy don’t affect the content of matrix, as you can see on the highlighted lines.
Finally, it’s important to note that when you have a list containing immutable objects, such as numbers, strings, or tuples, the behavior of deepcopy() mimics what copy() does:
>>> countries = ["United States", "Canada", "Poland", "Germany", "Austria"]
>>> nations = deepcopy(countries)
>>> id(countries) == id(nations)
False
>>> id(countries[0]) == id(nations[0])
True
>>> id(countries[1]) == id(nations[1])
True
In this example, even though you use deepcopy(), the items in nations are aliases of the items in countries. That behavior makes sense because you can’t change immutable objects in place. Again, this behavior optimizes the memory consumption of your code when you’re working with multiple copies of a list.
Updating Items in Lists: Index Assignments
Python lists are mutable data types. This means that you can change their elements without changing the identity of the underlying list. These kinds of changes are commonly known as in-place mutations. They allow you to update the value of one or more items in an existing list.
Note: To dive deeper into what mutable and immutable data types are and how they work in Python, check out Python’s Mutable vs Immutable Types: What’s the Difference?
To change the value of a given element in a list, you can use the following syntax:
list_object[index] = new_value
The indexing operator gives you access to the target item through its index, while the assignment operator allows you to change its current value.
Here’s how this assignment works:
>>> numbers = [1, 2, 3, 4]
>>> numbers[0] = "one"
>>> numbers
['one', 2, 3, 4]
>>> numbers[1] = "two"
>>> numbers
['one', 'two', 3, 4]
>>> numbers[-1] = "four"
>>> numbers
['one', 'two', 3, 'four']
>>> numbers[-2] = "three"
>>> numbers
['one', 'two', 'three', 'four']
In this example, you’ve replaced all the numeric values in numbers with strings. To do that, you’ve used their indices and the assignment operator in what you can call index assignments. Note that negative indices also work.
What if you know an item’s value but don’t know its index in the list? How can you update the item’s value? In this case, you can use the .index() method as in the code below:
>>> fruits = ["apple", "banana", "orange", "kiwi", "grape"]
>>> fruits[fruits.index("kiwi")] = "mango"
>>> fruits
['apple', 'banana', 'orange', 'mango', 'grape']
The .index() method takes a specific item as an argument and returns the index of the first occurrence of that item in the underlying list. You can take advantage of this behavior when you know the item that you want to update but not its index. However, note that if the target item isn’t present in the list, then you’ll get a ValueError.
You can also update the value of multiple list items in one go. To do that, you can access the items with the slicing operator and then use the assignment operator and an iterable of new values. This combination of operators can be called slice assignment for short.
Here’s the general syntax:
list_object[start:stop:step] = iterable
In this syntax, the values from iterable replace the portion of list_object defined by the slicing operator. If iterable has the same number of elements as the target slice, then Python updates the elements one by one without altering the length of list_object.
To understand these behaviors, consider the following examples:
>>> numbers = [1, 2, 3, 4, 5, 6, 7]
>>> numbers[1:4] = [22, 33, 44]
>>> numbers
[1, 22, 33, 44, 5, 6, 7]
In this example, you update the items located from 1 to 4, without including the last item. In this slice, you have three items, so you use a list of three new values to update them one by one.
If iterable has more or fewer elements than the target slice, then list_object will automatically grow or shrink accordingly:
>>> numbers = [1, 5, 6, 7]
>>> numbers[1:1] = [2, 3, 4]
>>> numbers
[1, 2, 3, 4, 5, 6, 7]
Now the initial list of numbers only has four values. The values 1, 2, and 3 are missing. So, you use a slice to insert them starting at index 1. In this case, the slice has a single index, while the list of values has three new values, so Python grows your list automatically to hold the new values.
You can also use a slice to shrink an existing list:
>>> numbers = [1, 2, 0, 0, 0, 0, 4, 5, 6, 7]
>>> numbers[2:6] = [3]
>>> numbers
[1, 2, 3, 4, 5, 6, 7]
Here, the initial list has a bunch of zeros where it should have a 3. Your slicing operator takes the portion filled with zeros and replaces it with a single 3.
Using the slicing operator to update the value of several items in an existing list is a pretty useful technique that may be hard to grasp at first. Go ahead and practice a bit more to get a deeper understanding of how this technique works.
Growing and Shrinking Lists Dynamically
In Python lists, mutability goes beyond allowing you to modify the items in place. Because lists are mutable, you can change their length on the fly by adding or removing elements. So, lists are also variable-length containers, as you already learned.
Adding new items to a list or removing unneeded ones are everyday tasks. That’s why Python provides different efficient ways to perform these actions. Using the right tool for the job is an essential skill.
In the following sections, you’ll explore the different tools that Python offers to grow and shrink a list dynamically.
Appending a Single Item at Once: .append()
The .append() method is probably the most common tool that you’ll use to add items to an existing list. As its name suggests, this method allows you to append items to a list. The method takes one item at a time and adds it to the right end of the target list.
Here’s an example of how .append() works:
>>> pets = ["cat", "dog"]
>>> pets.append("parrot")
>>> pets
['cat', 'dog', 'parrot']
>>> pets.append("gold fish")
>>> pets
['cat', 'dog', 'parrot', 'gold fish']
>>> pets.append("python")
>>> pets
['cat', 'dog', 'parrot', 'gold fish', 'python']
In these examples, every call to .append() adds a new pet at the end of your list. This behavior allows you to gradually populate an empty list or to add items to an existing list, as you did in the example.
Note: For a deep dive into how .append() works, check out Python’s .append(): Add Items to Your Lists in Place.
Using .append() is equivalent to doing the following slice assignment:
>>> pets[len(pets):] = ["hawk"]
>>> pets
['cat', 'dog', 'parrot', 'gold fish', 'python', 'hawk']
The slice assignment in this example grows your lists by appending a new item, "hawk", after the current last item in pets. This technique works the same as .append(). However, using .append() leads to a more readable and explicit solution.
An important fact to keep in mind when using .append() is that this method adds only a single item at a time. That item could be of any data type, including another list:
>>> pets.append(["hamster", "turtle"])
>>> pets
[
'cat',
'dog',
'parrot',
'gold fish',
'python',
'hawk',
['hamster', 'turtle']
]
Note how the last item in pets is a list of two pets rather than two new independent pets. This behavior may be a source of subtle errors. To avoid problems, you must remember that .append() takes and adds a single item each time.
If you need to add several items from an iterable at the end of an existing list, then you can use the .extend() method, which you’ll expore in the following section.
Extending a List With Multiple Items at Once: .extend()
When you’re working with lists, you may face the need to add multiple items to the right end of a list at once. Because this is such a common requirement, Python’s list has a dedicated method for that task.
The method is called .extend(). It takes an iterable of objects and appends them as individual items to the end of the target list:
>>> fruits = ["apple", "pear", "peach"]
>>> fruits.extend(["orange", "mango", "banana"])
>>> fruits
['apple', 'pear', 'peach', 'orange', 'mango', 'banana']
The .extend() method unpacks the items in the input iterable and adds them one by one to the right end of your target list. Now fruits has three more items on its end.
You should note that .extend() can take any iterable as an argument. So, you can use tuples, strings, dictionaries and their components, iterators, and even sets. However, remember that if you use a set as an argument to extend(), then you won’t know the final order of items beforehand.
Again, you must note that .extend() is equivalent to the following slice assignment:
>>> fruits = ["apple", "pear", "peach"]
>>> fruits[len(fruits):] = ["orange", "mango", "banana"]
>>> fruits
['apple', 'pear', 'peach', 'orange', 'mango', 'banana']
In this example, you use a slice assignment to add three items after the end of your original fruits list. Note that the result is the same as when you use .extend(). However, the .extend() variation is more readable and explicit.
Inserting an Item at a Given Position: .insert()
The .insert() method is another tool that you can use to add items to an existing list. This method is a bit different from .append() and .extend(). Instead of adding items at the right end of the list, .insert() allows you to decide where you want to put your item. That said, .insert() takes two arguments:
index: the index at which you want to insert the itemitem: the item that you need to insert into the list
When you insert an item at a given index, Python moves all the following items one position to the right in order to make space for the new item, which will take the place of the old item at the target index:
>>> letters = ["A", "B", "F", "G"]
>>> letters.insert(2, "C")
>>> letters
['A', 'B', 'C', 'F', 'G']
>>> letters.insert(3, "D")
>>> letters
['A', 'B', 'C', 'D', 'F', 'G']
>>> letters.insert(4, "E")
>>> letters
['A', 'B', 'C', 'D', 'E', 'F', 'G']
In this example, you insert letters into specific positions in letters. You must insert one letter at a time because .insert() adds a single item in every call. To insert an item, the method shifts all the items starting from the target index to the right end of the list. This shifting makes space for the new item.
As an exercise, could you come up with a slice assignment that produces the same result as .insert()? Click the collapsible section below for the solution:
Here’s the equivalent slicing of an insert operation:
list_object[index:index] = [item]
This statement takes an empty slice from list_object. Why empty? Well, the slicing starts at index and stops at index. Because of this, the slice doesn’t include any items.
Then the statement assigns a one-item list to the empty slice. This action results in item being inserted at index in list_object. Go ahead and give it a try!
Now that you’ve learned how to add items to an existing list using different tools and techniques, it’s time to learn how to remove unneeded items from a list, which is another common task.
Deleting Items From a List
Python also allows you to remove one or more items from an existing list. Again, deleting items from lists is such a common operation that the list class already has methods to help you with that. You’ll have the following methods:
| Method | Description |
|---|---|
.remove(item) |
Removes the first occurrence of item from the list. It raises a ValueError if there’s no such item. |
.pop([index]) |
Removes the item at index and returns it back to the caller. If you don’t provide a target index, then .pop() removes and returns the last item in the list. Note that the square brackets around index mean that the argument is optional. The brackets aren’t part of the syntax. |
.clear() |
Removes all items from the list. |
The .remove() method comes in handy when you want to remove an item from a list, but you don’t know the item’s index. If you have several items with the same value, then you can remove all of them by calling .remove() as many times as the item occurs:
>>> sample = [12, 11, 10, 42, 14, 12, 42]
>>> sample.remove(42)
>>> sample
[12, 11, 10, 14, 12, 42]
>>> sample.remove(42)
>>> sample
[12, 11, 10, 14, 12]
>>> sample.remove(42)
Traceback (most recent call last):
...
ValueError: list.remove(x): x not in list
The first call to .remove() deletes the first instance of the number 42. The second call removes the remaining instance of 42. If you call .remove() with an item that’s not in the target list, then you get a ValueError.
The .pop() method allows you to remove and return a specific item using its index. If you call the method with no index, then it removes and returns the last item in the underlying list:
>>> to_visit = [
... "https://realpython.com",
... "https://python.org",
... "https://stackoverflow.com",
... ]
>>> visited = to_visit.pop()
>>> visited
'https://stackoverflow.com'
>>> to_visit
['https://realpython.com', 'https://python.org']
>>> visited = to_visit.pop(0)
>>> visited
'https://realpython.com'
>>> to_visit
['https://python.org']
>>> visited = to_visit.pop(-1)
>>> visited
'https://python.org'
>>> to_visit
[]
In these examples, the first call to .pop() removes and returns the last site in your list of sites to visit. The second call removes and returns the first site, which is the site at index 0.
Finally, you use .pop() with -1 as an argument to emphasize that you can also use negative indices. This call removes and returns the last item. At the end of the process, your list of sites to visit is empty, pointing out that you’ve done all your planned visits.
Removing all the items from a list in one go can be another frequent task. In this case, Python also has you covered because list has a method called .clear(), which does exactly that. Consider the following example:
>>> cache = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
>>> cache.clear()
>>> cache
[]
If you call .clear() on a non-empty list object, then you get the list content completely removed. This method can be useful when your lists work as a cache that you want to quickly clean for a restart.
The following slice assignment produces the same result as the .clear() method:
>>> cache = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
>>> cache[:] = []
>>> cache
[]
In this slice assignment, you assign an empty list to a slice that grabs the whole target list. Again, this syntax is less explicit and readable than using .clear().
There’s still one more Python tool that you can use to remove one or more items from an existing list. Yes, that’s the del statement. You can combine del with an indexing or slicing operation to remove an item or multiple items, respectively:
>>> colors = [
... "red",
... "orange",
... "yellow",
... "green",
... "blue",
... "indigo",
... "violet"
... ]
>>> del colors[1]
>>> colors
['red', 'yellow', 'green', 'blue', 'indigo', 'violet']
>>> del colors[-1]
>>> colors
['red', 'yellow', 'green', 'blue', 'indigo']
>>> del colors[2:4]
>>> colors
['red', 'yellow', 'indigo']
>>> del colors[:]
>>> colors
[]
With the first del statement, you remove the color at index 1, which is "orange". In the second del, you use a negative index of -1 to remove the last color, "violet". Next, you use a slice to remove "green" and "blue".
Note: To dive deeper into using the del statement, check out Python’s del: Remove References From Scopes and Containers.
In the final example, you use del and a slice to remove all the items from an existing list. That construct produces a result that’s equivalent to calling .clear() on your target list.
Considering Performance While Growing Lists
When you create a list, Python allocates enough space to store the provided items. It also allocates extra space to host future items. When you use the extra space by adding new items to that list with .append(), .extend(), or .insert(), Python automatically creates room for additional new items.
This process is known as resizing, and while it ensures that the list can accept new items, it requires extra CPU time and additional memory. Why? Well, to grow an existing list, Python creates a new one with room for your current data and the extra items. Then it moves the current items to the new list and adds the new item or items.
Consider the following code to explore how Python grows a list dynamically: