Robert Johns | 13 Oct, 2023

Python Lists In-Depth Guide & Examples [2024] | Beginner to Pro

In this article, we’ve gone in-depth to cover everything you need about the python lists in 2024, including code examples to show you how to use them. 

As one of the top 3 languages, Python is one of the best choices for beginners and experienced devs alike in data science, web development, and much more. And when it comes to Python, knowing how to use python lists is essential.

Whether you’re new to learning Python or an experienced pro looking to level up your knowledge of python lists, we’ve covered everything from basic operations to comprehensions and Big-O performance!

Side note, if you’re new to Python and want to learn more, we’ve released our own Python course to help you truly learn the fundamentals of Python and to think like a pro.

So if you’re ready, let’s dive in and explore python lists!

Download Hackr.io's Python Lists In-Depth Guide PDF

What Is A Python List?

Beyond its basic function as a collection of items, the Python list is a powerful, mutable, and versatile data structure.

At its core, the Python list is an ordered collection of items, where each item can be of any object type. This makes lists one of the most versatile and commonly used data structures in Python. But what is a list? Is it a data type, a data structure, a sequence, or an interable?

The short answer is, yes, it’s all of these! And whatever Python project you’re working on, chances are, you’ll be using a list. Let’s explore some more.

Data Type, Data Structure, Sequence, & Iterable

The Python list represents the intersection of various computer science concepts in one simple but powerful object.

If you have your eyes on a career in any one of the many Python applications, knowing these details about the humble list will help you know when and how to use it like a pro. 

  • Data Type: Just as integers, floats, and strings are data types, a list is also a specific data type in Python, albeit a compound one.
  • Data Structure: Beyond being a data type, the list is a dynamic data structure. So, unlike arrays in other languages, a Python list can grow or shrink as needed.
  • Sequence: As an ordered collection, a list preserves the sequence of its items. This means that the order in which items are added to the list is the same order in which they can be retrieved.
  • Iterable Object: Lists are iterable, which means you can loop through each item using a for-loop. This is ideal for elegant and readable operations on list items.

Python List vs. Array

In most Python courses and tutorials, you’ll likely learn that the Python list is the Python version of an array. Which, for the most part, is fairly accurate.

That said, while Python lists are incredibly versatile, one might wonder how they differ from traditional arrays, especially since the terms are often used interchangeably in other programming contexts.

In many programming languages, an array is a collection of elements, all of which must be of the same type. This homogeneous restriction doesn't apply to Python lists, which can happily contain a mix of data types. Meaning that the list is heterogeneous. 

Which is a fancy way of saying, we can add any and all data types to a list.

Traditional arrays also have a fixed size, meaning you must specify the number of elements the array will hold ahead of time. Python lists, on the other hand, are dynamic in size.

In fact, if you ever plan to attain a Python certification, this is an excellent question that might be asked to quiz you on your true understanding of Python list.

Dynamic Sizing

Under the hood, a Python list is an array of pointers in C, each pointing to an object in memory. By implementing the CPython interpreter, Python lists offer dynamic sizing.

So, when you create a list and add items, Python will allocate more space than you need to anticipate future additions. If you keep adding elements beyond the allocated space, Python automatically reallocates the list by creating a larger array and copying the data.

This all happens automatically and behind the scenes, which is super helpful, but if you do plan to land a job with your Python skills, be prepared for this type of Python interview question.

The Importance Of Lists

Python lists are often at the center of Pythonic programming. With flexibility and efficiency, they’re the go-to data type for various tasks.

  • Data Collection: Lists are ideal for collecting and organizing data, whether user input, file contents, or data fetched from a database.
  • Iteration: Lists pair naturally with loops, making data processing and transformation tasks intuitive and efficient.
  • Functional Programming: With Python's lambda functions and list comprehensions, lists become even more powerful, enabling concise and readable data manipulation.
  • Bridging with Other Data Structures: Lists often serve as the starting point for creating other data structures like sets, dictionaries, and tuples.

This is by no means exhaustive, but the gist of this list is that it’s widely used. Just pick up any great Python book, and you’ll see the list is everywhere!

That said, let’s take a look at the key properties of the Python list. 

List Properties & Characteristics

Python lists are a staple in any coder's toolkit because they’re incredibly versatile. This power and adaptability stems from their intrinsic properties and characteristics.

Python Lists Are Mutable

Mutability refers to the ability of an object to change its state or content after its creation. When we say Python lists are mutable, it means you can alter their contents by adding, removing, or updating elements without the need to create a new list.

'''
Hackr.io Guide To Python Lists:
Mutable Property
'''
fruits = ['apple', 'banana', 'cherry']
print(f'Original List: {fruits}') 
# Output: Original List: ['apple', 'banana', 'cherry']

fruits[0] = 'grape'  # Changing the first element
print(f'Modified List: {fruits}') 
# Output: Modified List: ['grape', 'banana', 'cherry']

In this example, the first element originally containing apple changes to avocado without the need for creating a new list.

This is beneficial for data manipulation tasks, where the data needs to be transformed or updated based on specific conditions.

Elements Are Ordered

When we say that a list is ordered, we say that the sequence of elements in the list is preserved. 

In other words, the order in which items are added to the list is the exact order they will remain, unless explicitly modified.

It’s also really important to distinguish between being ordered and being sorted

An ordered collection retains its insertion order, but that doesn't mean it's sorted. A list can be ordered but in a completely random sequence.

'''
Hackr.io Guide To Python Lists:
Ordered Property
'''
numbers = [42, 7, 99, 1]
print(f'Original Order: {numbers}') 
# Output: Original Order: [42, 7, 99, 1]

numbers.append(56)  # Adding a new number to the end
print(f'New Order (with added element): {numbers}') 
# Output: New Order : [42, 7, 99, 1, 56]

In this example, the list is ordered (because it preserves the sequence of 42, 7, 99, and 1) but not sorted, as shown by the random sequence of the integer values.

This seems very obvious, but for beginners, it can be confusing when dealing with lists of numbers.

Indexability

A distinct property of Python lists is zero-based indexing. This means the first element is accessed with the index 0, the second with 1, and so on.

'''
Hackr.io Guide To Python Lists:
Indexable Property
'''
colors = ['red', 'green', 'blue']
print(f'First Color (index 0): {colors[0]}') 
# Output: First Color (index 0): red
print(f'Second Color (index 1): {colors[1]}') 
# Output: Second Color (index 1): green

This offers consistent and intuitive access to list items, especially when looping through elements using indices. 

However, you should always be cautious about accessing an index that doesn't exist, as it would raise an IndexError.

Heterogeneous Data Type

We touched on this fancy term earlier, but one of the standout features of Python lists is their ability to hold mixed object types. 

This property is termed as being heterogeneous, and it distinguishes Python lists from arrays in other programming languages that restrict you to a single type.

The TL-DR: A single Python list can hold integers, strings, floats, or even more complex data types like dictionaries, functions, or other lists, all in one instance.

'''
Hackr.io Guide To Python Lists:
Heterogeneous Property
'''
mixed_data = [42, 'hello world', 3.14, {'name': 'Alice'}, [1, 2, 3]]
print(f'Mixed List: {mixed_data}')
# Output: Mixed List: [42, 'hello world', 3.14, {'name': 'Alice'}, [1, 2, 3]]

This feature underscores the versatility of Python lists, making them suitable for a wide range of tasks, from collecting diverse data inputs to implementing multi-type algorithms.

Creating Python Lists

Whether you’re new to Python or an experienced dev, you’ll likely have been told that Python is renowned for its simplicity and user-friendly syntax. And as you’d expect, this is also true when it comes to creating a Python list.

So, if you’re ready, fire up your favorite Python IDE, and let’s start playing with lists!

Using Square Brackets

Square brackets [] are the most direct way to define a list in Python. When you wrap a sequence of values in these brackets, Python understands you're trying to create a list. 

This approach is known as creating a literal list, as shown below. Note that we can also create an empty list in this way.

'''
Hackr.io Guide To Python Lists:
Creating a List using Square Brackets
'''

# Defining an empty list
empty_lst = []

# List of integers
numbers = [1, 2, 3, 4, 5]

# List of strings
fruits = ['apple', 'banana', 'cherry']

print(empty_lst)  # Output: []
print(numbers)     # Output: [1, 2, 3, 4, 5]
print(fruits)      # Output: ['apple', 'banana', 'cherry']

Using the list() Constructor

Another way to create a Python list is with the list class constructor method. This is especially useful when converting other data types into lists. 

Note, however, that if you pass a string directly into the list constructor, the results may be surprising! This is because a string object is also a sequence of characters and an iterable object.

This means that if you pass a string argument into the list constructor, the individual characters will be iterated over and added as individual list elements, as shown below. 

This is a common Python mistake for beginners, so it helps to know what to expect.

'''
Hackr.io Guide To Python Lists:
Creating a List using the list() Constructor
'''

# Creating a list from a string
char_lst = list('apple')

# Creating a list from a tuple
tuple_lst = list((1, 2, 3, 4, 5))

print(char_lst)   # Output: ['a', 'p', 'p', 'l', 'e']
print(tuple_lst)  # Output: [1, 2, 3, 4, 5]

Basic List Operations

Now we know what a Python list is and how to create one, let’s take a closer look at the most common list operations that you will encounter in your day-to-day programming.

Accessing Elements

This is one of the most fundamental Python concepts for working with lists, and you can access list elements with positive or negative indices by wrapping them in square brackets [].

When using positive indices, the first element is at index 0, the second item is at index 1, and so on (recall zero-based indexing). We can then access a list element by wrapping the index in [].

Negative indices count from the end of the list, meaning that -1 refers to the last element in the list, -2 refers to the second to the last element, and so on. 

We can wrap a negative index in square brackets to access elements by referencing an element’s position in relation to the end of the list.

By understanding and using positive and negative indices, you can efficiently access list elements without knowing the list length. 

'''
Hackr.io Guide To Python Lists:
Accessing List Elements
'''

fruits = ['apple', 'banana', 'cherry', 'date']

# Accessing the first element
print(fruits[0])  # Output: apple

# Accessing the last element
print(fruits[-1])  # Output: date

Adding Elements

Being a mutable object type, we often need to add items to an existing list. By using the list object’s append() method, you can simply add a single item to the end of a list.

Important note: This method is only for adding items to the end of the list. This is great for speed, but less versatile if you want to add an item to a specific position in the list.

'''
Hackr.io Guide To Python Lists:
Appending an Element to the End
'''

fruits = ['apple', 'banana', 'cherry']
fruits.append('date')

print(fruits)  # Output: ['apple', 'banana', 'cherry', 'date']

If you want to insert an item to any position within an existing list, you can use the list object’s insert() method. Simply specify the index, followed by the item you want to insert, as shown below.

'''
Hackr.io Guide To Python Lists:
Inserting an Element at a Specific Position
'''

fruits = ['apple', 'banana', 'cherry']
fruits.insert(1, 'apricot')  # Insert 'apricot' at position 1

print(fruits)  # Output: ['apple', 'apricot', 'banana', 'cherry']

Another common scenario when dealing with Python lists is the need to merge two lists together or to add multiple items to an existing list in one pass. In this case, we can use the list object’s extend() method. 

We simply call the method on one list and then pass in another list as the argument, as shown below. The result is an extension to the original list upon which we called the method.

Note that we can pass in a list as an argument, or we can pass in a literal list of new items to add to the list. In either case, we extend the original list by adding the new items to the end of the list.

This is a lot like a multi-item version of append(), as we are adding the new items to the end of the original list.

'''
Hackr.io Guide To Python Lists:
Extending a List with Multiple Elements
'''

fruits = ['apple', 'banana', 'cherry']
extra_fruits = ['date', 'elderberry']

fruits.extend(extra_fruits)

print(fruits) 
# Output: ['apple', 'banana', 'cherry', 'date', 'elderberry']

fruits.extend(['fig, 'date])
print(fruits)
# Output: ['apple', 'banana', 'cherry', 'date', 'elderberry', 'fig', date']

Removing Elements

On the theme of mutability, another common requirement with the Python list is to remove elements. The simplest way to do this is with the list object’s remove() method.

Simply pass in an argument for the value that you want to drop, and the item is removed from the list, as shown below.

Important note: If you are new to Python, you might encounter scenarios where you have a list of integers. In these cases, it may be confusing to pass in an integer value to the remove() method, as it can appear like we’re passing in an index value. 

Just remember, the value you pass in is the item’s actual value.

'''
Hackr.io Guide To Python Lists:
Removing an Element by Value
'''

fruits = ['apple', 'banana', 'cherry', 'date']
fruits.remove('banana')

print(fruits)  # Output: ['apple', 'cherry', 'date']

Another useful way to remove items from a list is the list class pop() method. This will remove and return the value of the item at a given index.

Important note: Unlike the remove() method, pop() requires an index, not the value of the item.

We can also choose to omit the index, which by default, removes and returns the element at the last index position in the list.

Depending on your programming experience, pop() may be familiar if you’ve ever worked with stacks, queues, or Python deques, and with the list, it works in exactly the same way.

'''
Hackr.io Guide To Python Lists:
Using pop() to Remove and Return an Element
'''

fruits = ['apple', 'banana', 'cherry', 'date']
popped_fruit = fruits.pop(1)  # Removes and returns the second element

print(popped_fruit)  # Output: banana
print(fruits)        # Output: ['apple', 'cherry', 'date']

popped_fruit = fruits.pop()  # Removes and returns the last element
print(popped_fruit)  # Output: date
print(fruits)        # Output: ['apple', 'cherry']

Beyond the list methods we’ve covered above to remove list items, we can also use standard library built-in functions to remove elements. In this case, we can use the Python del statement. We simply follow the del statement with an indexed call to an item, as shown below.

This results in the item at this index being deleted from the list.

'''
Hackr.io Guide To Python Lists:
Deleting an Element Using del
'''

fruits = ['apple', 'banana', 'cherry', 'date']
del fruits[1]  # This will remove 'banana' from the list

print(fruits)  # Output: ['apple', 'cherry', 'date']

Find List Length

While there are several ways to find the length of a Python list, the len() function is the simplest way to determine how many items (elements) are within a list. This will always return an integer value to represent the number of items.

'''
Hackr.io Guide To Python Lists:
Using len() to Find List Length
'''

fruits = ['apple', 'banana', 'cherry', 'date']
print(len(fruits))  # Output: 4

List Membership

When it comes to Python operators, some of the most common that we use with lists are the in and not in operators to test element membership. 

By using these, we can determine whether or not an element is present in a list by evaluating the boolean return value, as shown below.

'''
Hackr.io Guide To Python Lists:
Using the "in" & "not in" Operator for Testing Membership
'''
# Create a list of fruits
fruits = ['apple', 'banana', 'cherry']

# Check if 'apple' is in the list
is_present = 'apple' in fruits
print(is_present)  # Output will be True

# Check if 'grape' is not in the list
is_absent = 'grape' not in fruits
print(is_absent)  # Output will be True

List Comparisons

While less common than the other list operations we’ve covered, we can also use Python’s comparison operators with lists.

These allow us to perform checks like equality (==), inequality (!=), and even lexicographical comparisons (<, <=, >, >=), as shown below.

'''
Hackr.io Guide To Python Lists:
Using Comparison Operators for Lists
'''
# Create two lists
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = [1, 2, 4]

# Check if lists are equal
are_equal = list1 == list2  # True
print(are_equal)

# Check if lists are unequal
are_unequal = list1 != list3  # True
print(are_unequal)

# Compare lists lexicographically
is_smaller = list1 < list3  # True
print(is_smaller)

Important note: One thing we need to point out is that order matters in list comparisons. 

All you need to know is that lists are compared element-wise from left to right. This means that if the first elements are equal, the second elements are then considered, and so on.

We should also touch on the idea of comparing lists that contain various element data types and nested lists.

Let’s start with mixed data types. In these scenarios, when performing comparisons, Python uses a consistent but type-specific order for built-in data types:

Numbers < Strings < Tuples < Lists < Dictionaries < Sets

However, if we have a list with mixed types and we try to do an element-wise comparison of incompatible types, such as numbers and strings, we will raise a TypeError, as shown below.

'''
Hackr.io Guide To Python Lists:
Comparing Lists with Mixed Data Types
'''
lst1 = [1, 'apple']
lst2 = ['1', 'apple']

# This comparison will raise a TypeError in Python 3
print(lst1 < lst2)

Now, let's tackle the subject of nested list comparisons. 

The general idea is that nested lists are lists that contain other lists as their elements. When comparing these, Python uses a recursive approach to compare lists element-by-element.

If the first elements of two lists are themselves lists, Python will compare those nested lists element-by-element. 

This continues until it encounters non-list elements or reaches the end of the nested lists. At this point, the comparison is carried out.

'''
Hackr.io Guide To Python Lists:
Comparing Nested Lists
'''
lst1 = [[1, 2], [3, 4]]
lst2 = [[1, 2], [3, 5]]

# Here, the first nested lists [1, 2] are equal, so Python moves on to compare [3, 4] and [3, 5].
# Since 4 < 5, list1 < list2 will return True.
print(lst1 < lst2)  # Output: True

lst3 = [[1, 3], [3, 4]]
lst4 = [[1, 2], [3, 5]]

# Here, the first nested lists are compared first, i.e., [1, 3] and [1, 2].
# Since 3 > 2, list3 > list4 will return True.
print(lst3 > lst4)  # Output: True

That said, here are some key points to bear in mind when comparing nested lists:

  • Comparisons are made based on the first differing element. If two lists are identical until one ends, the shorter list is considered smaller.
  • If the lists have different nesting depths (levels of nesting), Python will still perform a comparison down to the depth of the shorter list.

Advanced List Methods

Beyond the basic list operations we’ve already covered, there are several advanced list methods that can be helpful for more complex programming scenarios. 

Let’s take a look at some of these now, starting with the index() method. By using this, we can return the index of the first occurrence of a specified value in a list.

What does this mean? Well, as shown in the example below, we return an index value for an item that matches the value we’ve passed as an argument to the index() method.

Important note: If your list contains multiple instances of the same object, the index() method only returns the index of the first occurrence. What does this mean? 

Well, you can see below that our list contains two apple string values. If we were to use the index() method with apple, we would return a value of 0 to represent the first occurrence at index 0, but we would not know that we have another apple string in the last position.

'''
Hackr.io Guide To Python Lists:
Using index() to Find an Element's Index
'''

fruits = ['apple', 'banana', 'cherry', 'date', 'apple']
position = fruits.index('cherry')

print(position)  # Output: 2

Another useful list operation involves using the count() method to return the number of occurrences of a specified value within a list. Think of this as a way to get the frequency of a certain value.

Extending our previous example, by passing in the string value of apple to the count() method, we return an integer value of 2 to represent the two occurrences of this value.

'''
Hackr.io Guide To Python Lists:
Using count() to Count Occurrences of an Element
'''

fruits = ['apple', 'banana', 'cherry', 'date', 'apple']
frequency = fruits.count('apple')

print(frequency)  # Output: 2

One of the most common operations for data handling in general, is the need to reverse the order of existing data. 

Luckily for us, the Python list class has a built-in reverse() method to do just this. By calling this method on an existing list, we can modify (mutate) the list in place to reverse the order of the elements. 

Note that this does not return any values as it operates on an existing list.

'''
Hackr.io Guide To Python Lists:
Reversing a List In-Place Using reverse()
'''

fruits = ['apple', 'banana', 'cherry', 'date']
fruits.reverse()

print(fruits)  # Output: ['date', 'cherry', 'banana', 'apple']

Now, with that said, there are occasions when you want to reverse the data in a list without altering the original list itself.

In these cases, you can use the Python reversed() function instead of the list class reverse() method. For beginners, this might be confusing, especially as they look almost identical in name, but there’s a key difference between these two approaches.

By using the reversed() function, we can return an iterator that points to the last element of the original list. Then, depending on our needs, we can pass this iterator object to a list constructor to generate a new and reversed list, as shown below.

By doing this, we have reversed the data while also ensuring the original list remains unchanged. Pretty cool, right?!

Note that the reversed() function is not a list method, which means we do not call it upon a list, but rather we pass in a list as an argument to the function. We can then directly pass the returned iterator to a list constructor.

'''
Hackr.io Guide To Python Lists:
Using reversed() to Get a Reversed Iterator
'''

fruits = ['apple', 'banana', 'cherry', 'date']
reversed_fruits = list(reversed(fruits))

print(reversed_fruits)
# Output: ['date', 'cherry', 'banana', 'apple']
print(fruits)         
# Output: ['apple', 'banana', 'cherry', 'date'] (Original list unchanged)

Another of the most common operations on lists is the need to sort the data within. In these cases, we can use the list class sort() method to sort an existing list in place. Just like the reverse() method, this modifies (mutates) the original list, so tread carefully.

We can also customize this method to sort in ascending or descending order by using the reverse parameter within the method call, as shown below.

Note that if you are new to the concept of lexicographical ordering, you might find yourself wondering how lists of strings are sorted. Well, the short answer is that each character in a string has a code-point value (integer), and these are used to sort the strings.

'''
Hackr.io Guide To Python Lists:
Sorting a List In-Place Using sort()
'''

fruits = ['apple', 'banana', 'cherry', 'date']
fruits.sort()

print(fruits)
# Output: ['apple', 'banana', 'cherry', 'date'] (sorted in ascending order)

fruits.sort(reverse=True)
print(fruits)
# Output: ['date', 'cherry', 'banana', 'apple'] (sorted in descending order)

In the same way that we can choose to reverse list data without altering the original object, we can also sort list data without modifying the original list.

To do this, we can use the built-in Python sorted() function instead of the list sort() method. We simply pass in an existing list as an argument to this function to return a new sorted list object. This allows us to complete our sort without changing the original list.

Again, note how we use this function versus the list method. Rather than calling the function on a list object, we have to pass in the list object as an argument to the function. 

'''
Hackr.io Guide To Python Lists:
Using sorted() to Get a New Sorted List
'''

fruits = ['apple', 'banana', 'cherry', 'date']
sorted_fruits = sorted(fruits)

print(sorted_fruits)
# Output: ['apple', 'banana', 'cherry', 'date']
print(fruits)       
# Output: ['apple', 'banana', 'cherry', 'date'] (Original list unchanged)

Copies & Memory

Understanding the nuances of how Python lists interact with memory, along with the distinction between shallow and deep copies, is crucial for preventing unintended side effects in your programs. 

How Are Lists Stored In Memory?

Python lists are dynamic arrays. When you create a list, Python reserves a chunk of memory to store references to the objects contained in the list, not the actual objects themselves.

When the list grows beyond the reserved memory, Python will allocate a larger segment of memory, copy the references from the old location, and then point the variable to the new memory location. This ensures lists can dynamically grow as needed.

'''
Hackr.io Guide To Python Lists:
Storing Lists in Memory
'''

numbers = [1, 2, 3, 4, 5]
print(id(numbers))
# This will display the memory address of the list 'numbers'.

Shallow Copy

When you make a copy of a list, you have two primary options: a shallow copy and a deep copy.

When working with lists, especially nested structures, always be conscious of whether you need a shallow or deep copy to achieve your desired outcome.

Let’s start by looking at the concept of a shallow copy. 

By using the list class copy() method, we can create a new list, but importantly this approach doesn't create copies of the objects within the original list. 

What does this mean?

The new, copied list, contains references to the same objects, not duplicate elements. This means that nested lists (or other mutable objects) within the list will reflect changes across both the original and copied list.

What happens with lists of mutable objects?

If your list contains mutable objects, like other lists or dictionaries, modifications to these objects in the copied list will reflect in the original list, and vice versa.

What happens with lists of immutable objects?

Immutable objects within a copied list, like strings, numbers, or tuples, can be replaced without affecting the original list. This is because the reference to the immutable object is replaced in the copied list.

Deep Copy

Let’s now take a look at the concept of a deep copy by using the deepcopy() function from the copy module.

Now, unlike a shallow copy, this function creates a new list by recursively copying all of the objects within the original list. 

What does this mean?

A deep copy ensures that the copied list is entirely independent of the original, so any changes to nested structures within the copy will not affect the original list and vice versa. Again, we do not have duplicate elements, but a brand-new and independent list.

What about nested mutable objects?

In a deep copy, nested mutable objects in the copied list can be modified without any effect on the original list.

Are there any performance Implications?

While deep copies ensure data independence, they are more computationally intensive than shallow copies because they have to recreate every object, even those nested deeply. 

Put simply, when using deepcopy(), always weigh the need for a completely independent list against the potential performance overhead.

How To Copy A Python List

Suppose you want to create a shallow copy of a Python list? You actually have two ways to do this, starting with the list object’s copy() method.

By using the copy() method, we create a new list object to reference the same item objects as the original list. Again, these are not duplicate elements, but references to the same item objects.

Important note: While the new list is a separate object in memory, its items are references to the same items in the original list. This is the definition of a shallow copy.

Because the list is a shallow copy, changes to any mutable objects within the copy (like a nested list) are reflected in the original list, as shown below. 

However, as the two lists are distinct objects in memory, adding or removing items from one of the lists won't affect the other. So, if we append a new item to the copied list, this will not alter the original list, as shown below.

'''
Hackr.io Guide To Python Lists:
Using List's Built-in copy() Method
'''

# Original list with nested list
original_lst = [1, 2, ['apple', 'banana']]

# Creating a shallow copy using the built-in copy() method
copied_lst = original_lst.copy()

# Modifying the nested list in copied_list
copied_lst[2].append('cherry')

print(original_lst)        # Output: [1, 2, ['apple', 'banana', 'cherry']]
print(copied_lst)          # Output: [1, 2, ['apple', 'banana', 'cherry']]

# Modifying the copied_list
copied_lst.append('date')

print(original_lst)       
# Output: [1, 2, ['apple', 'banana', 'cherry']]
print(copied_lst)        
# Output: [1, 2, ['apple', 'banana', 'cherry'], 'date']

Another way to create a shallow copy of a Python list is with the copy() method from the copy module, as shown below. 

Notice that by modifying the nested list in shallow_copied_lst, we also altered the original_lst variable.

'''
Hackr.io Guide To Python Lists:
Creating a Shallow Copy Using copy()
'''

import copy

original_lst = [1, 2, [3, 4]]
shallow_copied_lst = copy.copy(original_lst)

shallow_copied_lst[2][0] = 99

print(original_lst)        # Output: [1, 2, [99, 4]]
print(shallow_copied_lst)  # Output: [1, 2, [99, 4]]

Now, let’s take a look at how to create a deep copy of a Python list by using the deepcopy() method from the copy module.

By looking at the example below, any changes we make to the nested list within deep_copied_list do not affect original_lst.This underlines the difference between a deep and shallow copy.

'''
Hackr.io Guide To Python Lists:
Creating a Deep Copy Using deepcopy()
'''

import copy

original_list = [1, 2, [3, 4]]
deep_copied_list = copy.deepcopy(original_list)

deep_copied_list[2][0] = 99

print(original_list)        # Output: [1, 2, [3, 4]]
print(deep_copied_list)     # Output: [1, 2, [99, 4]]

Slicing And Dicing Lists In Python

Python lists are incredibly versatile, and one of the most useful features they offer is slicing. Slicing allows you to access a specific subset of a list, which is especially handy for tasks like data processing, parsing, and many more. 

Let’s break down the basics of list slicing, including the role of step values and the power of negative indexing.

Basic Slicing

At its core, slicing a list is about specifying two indices: a starting point and a stopping point, as shown below.

'''
Hackr.io Guide To Python Lists:
Slicing Syntax
'''
lst_slice = lst[start:stop]

Important notes: 

  • The starting index is inclusive, while the stopping index is exclusive.
  • If we omit the start index, the slice starts at index 0
  • If we omit the stop index, the slice continues to the last index in the list 
  • If we omit the start and stop index, we return all the elements

By using the slice operator, we can then include elements from the starting point up to, but not including, the stopping point.

'''
Hackr.io Guide To Python Lists:
Basic Slicing
'''
# Creating a list of numbers
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# Slicing from index 2 to 7
subset = numbers[2:7]
print(subset)  # Output: [2, 3, 4, 5, 6]

Step in Slicing

To further refine our slicing, Python introduces the concept of a step value, as shown in the syntax below.

'''
Hackr.io Guide To Python Lists:
Slicing Syntax With Step
'''
lst_slice = lst[start:stop:step]

By default, the step is set to 1, meaning each element is considered in the slice. However, by adjusting the step value, you can skip elements or even reverse the slice.

'''
Hackr.io Guide To Python Lists:
Step in Slicing
'''
# Slicing with a step of 2 (selecting every second element)
subset_step = numbers[2:8:2]
print(subset_step)  # Output: [2, 4, 6]

# Reversing a list using slicing
reversed_numbers = numbers[::-1]
print(reversed_numbers)  # Output: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

Note that in the example above, we’ve used a common Python trick where we use [::-1] to reverse a Python list.

To break this down, we’ve omitted the start index and the stop index, which means that we want to slice from the start of the list to the end of the list. We’ve then used a negative step value to return all the elements in reverse order. Cool, right?!

Negative Indexing

As we’ve already seen earlier, Python lists support negative indexing which allows us to refer to elements from the end of the list, where an index of -1 refers to the last item, -2 to the second last, and so on.

We can also use these ideas when slicing, however, we should point out that when using negative indexing, the same principles apply. This means that the slice includes the start and excludes the stop.

'''
Hackr.io Guide To Python Lists:
Negative Indexing
'''
# Accessing the last element
last_element = numbers[-1]
print(last_element)  # Output: 9

# Slicing the last three elements
last_three = numbers[-3:]
print(last_three)  # Output: [7, 8, 9]

# Slicing with negative start, stop, and step values
negative_slice = numbers[-5:-2:2]
print(negative_slice)  # Output: [5, 7]

Nested Lists In Python

Nested lists, or lists within lists, are common structures in Python that offer a way to represent more complex data hierarchies, like matrices, tables, or even tree structures. 

Creating Nested Lists

A nested list is essentially a list where one or more of its elements is a list. The creation of nested lists is straightforward and is as simple as placing lists inside other lists.

The example below illustrates a 3x3 matrix represented as a nested list.

'''
Hackr.io Guide To Python Lists:
Creating Nested Lists
'''
# Sample nested list representing a matrix
matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
]

print(matrix)
# Output: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Accessing Elements from Nested Lists

To access elements within a nested list, you use multiple indices. The first index corresponds to the outer list, the second index to the first nested list, the third index to the list nested within that, and so forth.

The key is to work from the outermost list inwards when accessing or modifying elements.

'''
Hackr.io Guide To Python Lists:
Accessing Elements from Nested Lists
'''
# Accessing the second element of the first list (2 in the matrix)
element = matrix[0][1]
print(element)  # Output: 2

# Accessing the third element of the third list (9 in the matrix)
element_2 = matrix[2][2]
print(element_2)  # Output: 9

List Comprehensions

Python list comprehensions provide a concise and readable way to create lists. They not only simplify code but often make it more efficient, offering the power to transform and filter data on the fly. 

Basic List Comprehensions

The basic syntax for a python list comprehension is shown below.

'''
Hackr.io Guide To Python Lists:
List Comprehension Syntax
'''
[expression for item in iterable]

In simple terms, this iterates over each element in the given iterable, applies an expression to it, and then appends the result to a new list, as shown in the example below.

'''
Hackr.io Guide To Python Lists:
Basic Comprehension
'''
# Creating a list of squares of the first ten natural numbers
squares = [x ** 2 for x in range(1, 11)]
print(squares)  # Output: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

The list comprehension shown above is the equivalent of using the following for loop.

squares = []
for x in range(1, 11):
  squares.append(x ** 2)

Adding Conditions: Filtering

You can also add if conditions to a comprehension to filter elements, with the general form shown below.

'''
Hackr.io Guide To Python Lists:
List Comprehension Filtering Syntax
'''
[expression for item in iterable if condition]

By using this syntax in the example below,  we can use a list comprehension to create a list of squares of even numbers between 1 and 10.

'''
Hackr.io Guide To Python Lists:
Adding Conditions for Filtering
'''
# Creating a list of squares of the first ten natural numbers that are even
even_squares = [x ** 2 for x in range(1, 11) if x % 2 == 0]
print(even_squares)  # Output: [4, 16, 36, 64, 100]

Nested List Comprehensions

List comprehensions can also be nested to handle lists that contain other lists (nested lists). This approach can be especially powerful when working with matrices, grids, or any multi-dimensional data.

That said, nested list comprehensions can sometimes become hard to read. Always aim for readability when using advanced features like nesting - remember the zen of Python!

'''
Hackr.io Guide To Python Lists:
Nested List Comprehensions
'''
# Original 2x3 matrix
matrix = [
  [1, 2, 3],
  [4, 5, 6]
]

# Transposing the matrix using nested list comprehension
transposed_matrix = [
  [row[i] for row in matrix]
  for i in range(len(matrix[0]))
]
print(transposed_matrix)  # Output: [[1, 4], [2, 5], [3, 6]]

List Traversal & Iteration

Traversal and iteration over lists are foundational concepts in Python, and for good reason. They provide the means to process, modify, or analyze each element within a list. 

Whether you're summing values, looking for matches, or building new lists, iteration tools in Python make these tasks intuitive and efficient. In this guide, we will explore three popular methods: using for loops, employing enumerate(), and leveraging list comprehension.

Using For Loops

The most basic way to iterate over a list is by using a for loop. This allows you to perform actions on each element within the list, as each item is returned a loop variable for each successive iteration.

This is all possible because lists are iterable objects (they implement the __iter__ dunder method).

'''
Hackr.io Guide To Python Lists:
Using for Loops
'''
# Sample list of fruits
fruits = ['apple', 'banana', 'cherry']

# Using a for loop to print each fruit
for fruit in fruits:
  print(fruit)
# Output:
# apple
# banana
# cherry

Using enumerate()

Sometimes, while iterating, you may want to have access to both the index and the value of an item. This is where enumerate() comes in handy, as it returns the index and the value of each element during each iteration, as shown below.

'''
Hackr.io Guide To Python Lists:
Using enumerate() for Index and Value
'''
# Iterating over fruits with enumerate
for index, fruit in enumerate(fruits):
  print(f'Index {index} has fruit: {fruit}')
# Output:
# Index 0 has fruit: apple
# Index 1 has fruit: banana
# Index 2 has fruit: cherry

List Comprehensions For Iteration

List comprehension provides a compact way to create lists. This is a unique and Pythonic approach to iterate over lists (or other iterable objects) and perform operations, often leading to more readable and efficient code.

Once you get to grips with them, you’ll find they’re one of your favorite features in Python!

List comprehensions can also be nested, allowing for powerful and concise expressions. However, it's also essential to maintain readability when doing so - remember the zen of Python!

'''
Hackr.io Guide To Python Lists:
List Comprehensions For Iteration
'''
# Creating a new list with the lengths of each fruit
lengths = [len(fruit) for fruit in fruits]
print(lengths)  # Output: [5, 6, 6]

# Using list comprehension with conditions
# Getting fruits that have more than 5 characters
long_fruits = [fruit for fruit in fruits if len(fruit) > 5]
print(long_fruits)  # Output: ['banana', 'cherry']

Big-O Performance

For anyone working with algorithms and data structures, understanding Big-O notation is essential for understanding how operations scale with increasing data size. 

When it comes to Python lists, Big-O performance varies based on the operation in question, as shown in the table below.

Operation

Description

Time Complexity

Indexing with []

Accessing an element in a list by its index is constant time, no matter how long the list is.

O(1) - Constant time.

append()

Appending an item to the end of a list is usually constant time. However, occasional reallocations due to resizing can make it slower.

O(1) on average, O(n) in rare cases.

insert()

Inserting an item at a particular index requires shifting all subsequent items by one place.

O(n) - Linear time.

remove()

This operation searches for a value and removes the first occurrence. Since the list might need to be iterated to find the value, and elements shifted upon removal, it's linear time.

O(n) - Linear time.

pop()

Popping an item from the end is fast. Popping from the beginning or middle is slower due to the required shifting.

O(1) for the end, O(n) otherwise.

extend()

This operation appends all the items from one list onto another.

O(k) where k is the length of the list being added.

index()

This finds the index of a value. In the worst case, Python checks each element.

O(n) - Linear time.

count()

Counts the number of occurrences of a value, requiring a full scan in the worst case.

O(n) - Linear time.

sort()

Python lists are sorted using an adaptive, stable, and efficient sorting algorithm derived from Timsort.

O(n log n) - Log-linear time.

reverse()

Reversing a list in place requires switching around all its items.

O(n) - Linear time.

copy()

Creating a shallow copy of a list involves duplicating all its items.

O(n) - Linear time.

'''
Hackr.io Guide To Python Lists:
Time Complexity of Common Operations
'''
# Sample list for demonstration
sample_lst = [1, 2, 3, 4, 5]

# Indexing with [] has O(1) time complexity
element = sample_lst[2]

# append() has O(1) time complexity
sample_lst.append(6)

# insert() has O(n) time complexity
sample_lst.insert(2, 7)

# remove() has O(n) time complexity
sample_lst.remove(7)

# pop() has O(1) time complexity for the last element
sample_lst.pop()

# extend() has O(k) time complexity
sample_lst.extend([7, 8, 9])

# index() has O(n) time complexity
index_of_three = sample_lst.index(3)

# count() has O(n) time complexity
count_of_three = sample_lst.count(3)

# sort() has O(n log n) time complexity
sample_lst.sort()

# reverse() has O(n) time complexity
sample_lst.reverse()

# copy() has O(n) time complexity
copied_lst = sample_lst.copy()

Python List Tips & Tricks

Python lists are versatile and packed with features that many developers aren't aware of, and knowing some of the lesser-known operations can make your code shorter, cleaner, and more efficient. 

Let’s take a look at five techniques you need to add to your Python cheat sheet to enhance your list manipulations in any Python program. 

Using * For List Repetition

The * operator allows you to repeat a list multiple times, making it handy for initializing lists with default values. 

This is also useful to pre-populate a list with None object placeholders if you want a list of a predetermined length that will be filled in later.

'''
Hackr.io Guide To Python Lists:
Using * for List Repetition
'''
# Repeating a list 3 times
repeated_lst = [1, 2, 3] * 3
print(repeated_lst)  # Outputs: [1, 2, 3, 1, 2, 3, 1, 2, 3]

# Initializing a list of size 5 with None
placeholder_lst = [None] * 5
print(placeholder_lst)  # Outputs: [None, None, None, None, None]

Using zip() To Merge Lists

The Python zip() function allows you to merge two or more iterable objects, including two lists. Simply pass in two list arguments to the function to return an iterator of tuples, where the i-th tuple contains the i-th element from each of the input lists.

You can then pass this iterator as an argument to the list constructor to create a new list of tuple pairs, where each pair contains corresponding elements on an index-by-index basis.

'''
Hackr.io Guide To Python Lists:
Using zip() to Merge Lists
'''
lst_a = ['a', 'b', 'c']
lst_b = [1, 2, 3]
merged_lst = list(zip(lst_a, lst_b))
print(merged_lst)  # Outputs: [('a', 1), ('b', 2), ('c', 3)]

Flattening Nested Lists

Nested lists (or lists within lists) are very common, but sometimes, these can be optimized by using a process called flattening. This means turning a nested list into a 1D list.

To do this,  we can use a combination of list comprehensions and the unpacking operator * (which we’ll cover in a moment!), as shown below.

'''
Hackr.io Guide To Python Lists:
Flattening Nested Lists
'''
nested_lst = [[1, 2, 3], [4, 5], [6, 7, 8]]
flattened_lst = [item for sublst in nested_lst for item in sublst]
print(flattened_lst)  # Outputs: [1, 2, 3, 4, 5, 6, 7, 8]

List Unpacking

Unpacking is a process where we can assign multiple variables from a list in a single line (this also works with tuples). This is an elegant way to concisely extract values from a list.

'''
Hackr.io Guide To Python Lists:
List Unpacking
'''
data_lst = ['Alice', 28, 'Engineer']
name, age, profession = data_lst
print(name)        # Outputs: Alice
print(age)         # Outputs: 28
print(profession)  # Outputs: Engineer

Avoiding Concatenation With +

Many Python beginners (and even seasoned devs) have probably found themselves using the + operator to concatenate two or more lists. On the surface, this seems fine, but the implications are non-negligible as the size of the lists grows very large.

Yes, I’m referring to Big-O performance, and the main takeaway is that when we use the + operator to concatenate lists, we have to create a new list and copy objects from both lists into it. This is inefficient, especially with large lists. 

A better approach is using the extend() method, as shown below.

'''
Hackr.io Guide To Python Lists:
Avoiding Concatenation with +
'''
lst_one = [1, 2, 3]
lst_two = [4, 5, 6]

# Using extend()
lst_one.extend(lst_two)
print(lst_one)  # Outputs: [1, 2, 3, 4, 5, 6]

Common Errors and Pitfalls with Python Lists

While Python's lists are incredibly versatile and user-friendly, many developers, especially beginners, can encounter certain mistakes and misunderstandings. 

To help you navigate your way out of these tricky situations, let’s take a look at some common pitfalls with Python lists.

Modifying A List While Iterating Over It

One of the most common pitfalls is trying to modify a list by adding or removing elements while iterating over it. This can lead to unexpected behavior or errors, as shown in the example below.

In this case, we try to pop elements from the list if they have an even-numbered index. The problem is that the for loop will always try to iterate based on the original list length.

This means that if we reduce the length of the list, we will try to access an index that is no longer in the list, and we will encounter an IndexError.

'''
Hackr.io Guide To Python Lists:
Modifying a List While Iterating
'''
numbers = [1, 2, 3, 4, 5]

for i in range(len(numbers)):
  # Step 1: i=0, numbers = [1, 2, 3, 4, 5]
  # Step 2: i=1, numbers = [1, 3, 4, 5]   (after removing 2)
  # Step 3: i=2, numbers = [1, 3, 5]     (after removing 4)
  # Step 4: i=3, IndexError as length is now only 3
  if numbers[i] % 2 == 0:
      numbers.pop(i)

print(numbers)

The way to avoid this issue is to use a copy of the original list, as shown below. In this case, we reference the list copy as our loop variable, which avoids any index issues as we do not modify the length of the list copy.

'''
Hackr.io Guide To Python Lists:
Modifying a List Using a Copy
'''
numbers = [1, 2, 3, 4, 5]

for num in numbers.copy():
  if num % 2 == 0:
      numbers.remove(num)

print(numbers)  # Outputs: [1, 3, 5]

Confusion Between Shallow And Deep Copy

When copying lists, it's crucial to understand the difference between a shallow copy and a deep copy. 

A shallow copy creates a new list, but does not create copies of the objects within the list. A deep copy, on the other hand, creates duplicates of every item. We’ve covered this above, but it bears repeating as it’s one of the easiest traps to fall into with Python lists.

'''
Hackr.io Guide To Python Lists:
Shallow vs Deep Copy
'''
import copy

original = [[1, 2, 3], [4, 5, 6]]

# Shallow copy
shallow_copied = original.copy()
shallow_copied[0][0] = 99
print(original[0])
# Outputs: [99, 2, 3], demonstrating that the original list was modified

# Deep copy
deep_copied = copy.deepcopy(original)
deep_copied[0][0] = 11
print(original[0])
# Outputs: [99, 2, 3], demonstrating that the original list remains unchanged

Index Out Of Range

Trying to access a list element using an index that doesn't exist will result in an IndexError. We saw an example of this above when trying to modify a list while traversing over its elements. 

That said, this is a very common error, especially when working with loops or dynamic list operations. 

'''
Hackr.io Guide To Python Lists:
Index Out of Range
'''
my_list = [1, 2, 3]
try:
  print(my_list[3])  # IndexError, since valid indices are 0, 1, and 2
except IndexError:
  print('Index out of range!')

The only way to avoid this error is to always ensure you're accessing valid indices. One way to do this is to check the length of a list using the len() function. You can then ensure your index is within the range of 0 and the length -1.

Wrapping Up

So there you have it, you now know python lists inside and out! We’ve even included source code examples to show you how to work with python lists, including basic and advanced operations, list comprehensions, slicing, and more.

Whether you’re just starting out on your Python journey or an experienced dev looking to boost your skills, we’ve covered python lists for all skill levels.

We hope you’ve enjoyed learning about python lists, and if you have any interesting thoughts about python lists, let us know in the comments!

Enjoyed learning about Python lists and ready to dive deeper into Python? Check out:

Our Python Masterclass - Python with Dr. Johns

Frequently Asked Questions

1. What Are Lists For In Python?

Python lists are dynamic arrays that can hold multiple items of various data types, including numbers, strings, and other objects. Lists are mutable, meaning contents can be modified after creation, and they are a core data structure for organizing and managing sequential data.

2. What Is The List [- 1 :] In Python?

This is a Python slice operation that targets the last element of a list, but rather than returning just the item, it returns a new list containing only that item. This is helpful when you want to work with sublists or maintain a list structure when retrieving elements.

3. What Is List [- 2] In Python?

Negative indices count from the end of the list, so this expression provides direct access to the second-to-last item.

4. What Is The Syntax For List In Python?

In Python, the syntax for defining a list involves using square brackets [] with items separated by commas. Lists can be initialized empty or with initial values. You can also use the list() constructor to create a list by passing in an iterable object argument.

People are also reading:

By Robert Johns

Technical Editor for Hackr.io | 15+ Years in Python, Java, SQL, C++, C#, JavaScript, Ruby, PHP, .NET, MATLAB, HTML & CSS, and more... 10+ Years in Networking, Cloud, APIs, Linux | 5+ Years in Data Science | 2x PhDs in Structural & Blast Engineering

View all post by the author

Subscribe to our Newsletter for Articles, News, & Jobs.

Thanks for subscribing! Look out for our welcome email to verify your email and get our free newsletters.

Disclosure: Hackr.io is supported by its audience. When you purchase through links on our site, we may earn an affiliate commission.

In this article

Learn More

Please login to leave comments