Avoid This¶
This section includes some pitfalls to avoid when you're writing Python that I've seen crop up in student code in 6.101.
Don't assign the output of functions that don't return anything¶
Pay attention to what functions in Python return! list.append, set.remove,
set.add, and list.sort are some common functions that you might use in your
programs, and they all return None
! Instead, they modify the underlying
object in-place. Make sure to never assign the output of any of these
function calls because they don't return any value.
This means this example below won't return anything and causes an error instead.
1 2 3 4 5 |
|
If we tried this example in our REPL, we would see this error, due to
list_to_sort.sort()
returning None
.
>>> unsorted = (7, 5, 3, 9, 12)
>>> sort_tuple(unsorted)
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
File "<stdin>", line 5, in sort_tuple
TypeError: 'NoneType' object is not iterable
However, we can reference the original variable after calling list.sort on it, and now the function will work properly.1
1 2 3 4 5 |
|
>>> sort_tuple(unsorted)
(3, 5, 7, 9, 12)
Boolean Laundering / Unnecessary nesting¶
You might find places in your code where code can be simplified or condensed.
One way we can often reduce code is by relying on truthiness of data types. Say
we want to check whether a list data
has any values in it to run a function.
We could do:
if len(data) > 0:
print("list has values!")
However, a more elegant way to do this would be to rely on the fact that lists with elements are truthy, like so:
if data:
print("list has values!")
This also works for dictionaries and sets.
Another thing to look for when condensing code is boolean laundering, where
we manually check the value of a condition and then return True
or False
based on it. See this example:
def func():
if condition:
return True
else:
return False
Notice that if condition
is True
, we return True
, and if it's False
, we
return False
. These 4 lines can be condensed into 1 if we just do:
def func():
return condition
Another thing we can check for is unnecessary nesting of if statements. If we want to run a function based off of multiple conditions, you might have several if statements nested like this:
if condition1:
if condition2:
if condition3:
do_thing()
Notice that do_thing()
will only run if condition1
and condition2
and
condition3
are all true. We can combine these with the and
operator, like
so.2
if condition1 and condition2 and condition3:
do_thing()
When to use i
as an iterator¶
In many programming languages, it's convention to use i
as an iterator, which
is why you'll see for i in range(n)
as the first line of many for
loops in Python. However, you should only use i
when you're iterating over a
range of integers, to match convention with other languages. If you're iterating
over other data structures, try to pick a variable name that captures what that
element represents, like in the following examples:
alphabet = "abcdefghijklmnopqrstuvwxyz"
for letter in alphabet:
pass
scores = [12, 57, 35, 17]
for i, score in enumerate(scores):
print(f"Player {i} has {score} points.")
people = ["Cameron", "Jeff", "Sarah"]
for person in people:
pass
Return inside a loop¶
Python uses the indentation of your code to organize it into blocks. This link has a diagram of what that looks like. This means that your code will behave differently based on its indentation level, so you should pay attention to how indented lines of code are.
Say we create a function to increment a counter, and we want to return the last value.
1 2 3 4 5 |
|
Because of how it's indented, the return statement on line 5 is inside the while loop. This means that the function will return on the first iteration of the while loop, preventing the loop from continuing. Without changing any code, we can change the indentation level of the last line to make it return after the loop is finished.
1 2 3 4 5 |
|
Be careful whenever you put return
statements around loops, as returning a
function inside of a loop will stop its execution and might not be what you
intend to do.
Making Unnecessary Copies¶
Functions that update an object in-place will generally be much faster another
function that returns a copy of that data structure. As an example, consider the
following code that adds all numbers under a million to a set b
.
1 2 3 |
|
Line 3 will create a new set (containing b | {i,}
) on every single iteration
and reassign the b
variable to that new set. Creating all those new sets takes
a long time to finish, and in reality, we just want to add some new values to
the b
set.
Fortunately, we have lots of options for how to fix this line of code. An easy
one would be to replace line 3 with our handy |=
operator from the
Useful Functions section.
b |= {i,}
If you remember, this operator is identical to set.update, so we could write line 3 this way as well:
b.update({i,})
Finally, we could also just use the set.add function you're familiar with to add a single element to our set.
b.add(i)
-
The best way to sort tuples would be to use the sorted function and convert to a tuple, like
tuple(sorted(unsorted))
, but for this example, we convert to a list so we can use list.sort. ↩ -
Because of Python's short circuit evaluation of
and
,condition2
will not be evaluated ifcondition1
is False, so these two code samples work exactly the same! ↩