2024-08-26 16:00:15 +02:00

157 lines
4.6 KiB
Plaintext

An `IndexError` is a bug class that occurs in Python when a program tries to
access a list, a tuple or other types of sequences at an index that does not
exist.
This bug can cause your program to crash or behave unexpectedly.
To fix an `IndexError`, you should always ensure you are accessing sequences
within their bounds.
== Why is this an issue?
An `IndexError` indicates a bug or a logical error in the code.
In Python, lists and tuples have a certain length and their elements are indexed
starting from `0`, counting up to the last index that is still smaller than the
length.
When trying to access a list or tuple with an index outside of this range,
an `IndexError` will be raised and the operation will fail.
Negative indices are supported. When using a negative index, it will be
interpreted by computing the sum of the negative index and the list size.
The result is then used as the actual index for accessing the sequence.
Thus, this sum must be non-negative and fit into the aforementioned range.
include::../impact.adoc[]
== How to fix it
=== Code examples
The following examples contain out-of-bounds accesses to sequences, resulting in
an `IndexError`.
These situations can be avoided by carefully considering the range of valid
index values, or even better, by comparing indices against the length of a
sequence.
In this first example, the list `ls` contains three elements.
Since the first element of a list has index `0`, the last valid index is `2`.
Therefore, an `IndexError` is raised when accessing `ls` at index `3`.
==== Noncompliant code example
[source,python,diff-id=1,diff-type=noncompliant]
----
def fun():
ls = [1, 2, 3]
foo(ls[3]) # Noncompliant: the last index of the list ls is 2. Using the index 3 will raise an IndexError.
----
==== Compliant solution
[source,python,diff-id=1,diff-type=compliant]
----
def fun():
ls = [1, 2, 3]
foo(ls[2])
----
Accessing a list with its length as the index is never correct:
==== Noncompliant code example
[source,python,diff-id=2,diff-type=noncompliant]
----
def fun():
ls = [1, 2, 3]
print(ls[len(ls)]) # Noncompliant: Indexing starts at 0, hence the list length will always be an invalid index.
----
==== Compliant solution
[source,python,diff-id=2,diff-type=compliant]
----
def fun():
ls = [1, 2, 3]
# We can make sure ls is non-empty before trying to access its last element.
# Also, the index `len(ls) - 1` or just `-1` will correctly select the last
# element within bounds.
print("Empty list!" if not ls else ls[-1])
----
=== How does this work?
You should always ensure that you are accessing sequences using indices that are
within the bounds of the sequence.
Here are some best practices to follow:
Always compare indices against the length of a sequence using `if-else`
statements or other control flow constructs before accessing elements.
The length can be retrieved using the built-in `len(...)` function.
For example, `len(ls)` returns the length of the list `ls`.
Use loops to iterate over sequences instead of accessing elements directly.
For example, `for elem in ls:` iterates over the list `ls`.
=== Pitfalls
The indices `0`, `len(...) - 1`, or `-1` for the first and last element of a
sequence are not always valid!
Make sure the sequence in question is non-empty before accessing these
indices.
=== Going the extra mile
Built-in functions like `map()`, `filter()`, and `reduce()` present additional
ways to operate on sequences without using indices.
If you absolutely need to know the index of an element while iterating over a
sequence, you can use `enumerate()`. It provides you the indices and the
elements of a sequence during iteration, eliminating the need to manually
retrieve elements from the sequence using indices.
==== Noncompliant code example
[source,python,diff-id=3,diff-type=noncompliant]
----
for i in range(len(ls)):
elem = ls[i] # We can eliminate this access by index using enumerate.
foo(i, elem)
----
==== Compliant solution
[source,python,diff-id=3,diff-type=compliant]
----
for i, elem in enumerate(ls):
foo(i, elem)
----
== Resources
=== Documentation
* https://docs.python.org/3/reference/expressions.html#subscriptions[Subscriptions]
* https://docs.python.org/3/library/exceptions.html#IndexError[IndexError]
* https://docs.python.org/3/library/functions.html#built-in-functions[Built-ins, including `map`, `filter`, `enumerate`, etc.]
ifdef::env-github,rspecator-view[]
'''
== Implementation Specification
(visible only on this page)
=== Message
Fix this access on a list element that may trigger an "IndexError".
Fix this access on a tuple element that may trigger an "IndexError".
'''
endif::env-github,rspecator-view[]