In-Place vs Return Value
Concepts covered: List Modification
Python list methods follow a convention: a method that changes the list in place returns None. That None signals that the work happened to the object you called it on. Functions and operators that leave the original alone hand back something new instead. Knowing which group a call belongs to tells you both what happens to your list and what you get back.
numbers = [3, 1, 4, 1, 5]
result = numbers.sort()
print("result:", result)
print("numbers:", numbers)result: None numbers: [1, 1, 3, 4, 5]
numbers.sort() reordered numbers and returned None, which landed in result. To keep the original order and get a sorted version, call the built-in sorted(), which reads any iterable and returns a new list.
original = [3, 1, 4, 1, 5]
ordered = sorted(original)
print("original:", original)
print("ordered:", ordered)
print("same object:", original is ordered)original: [3, 1, 4, 1, 5] ordered: [1, 1, 3, 4, 5] same object: False
| Call | Changes the list? | Returns |
|---|---|---|
| `lst.append(x)`, `lst.extend(xs)`, `lst.insert(i, x)` | Yes | `None` |
| `lst.remove(x)`, `lst.clear()` | Yes | `None` |
| `lst.sort()`, `lst.reverse()` | Yes | `None` |
| `lst.pop()` | Yes | The removed item |
| `lst.copy()`, `lst[a:b]`, `lst + other` | No | A new list |
| `sorted(lst)` | No | A new list |
| `reversed(lst)` | No | An iterator over the list |
| `lst.index(x)`, `lst.count(x)` | No | A number |
pop() is the in-place method with a useful return value, the item it removed, and stacks rely on it. reversed(lst) returns an iterator that reads the list backwards on demand; wrap it in list() to get a new list.
Assigning the result of an in-place call
Writing rows = rows.append(x) stores the None that append returned, and the name no longer reaches the list. The next method call on rows fails, often several lines after the line with the mistake.
rows = ["r1"]
rows = rows.append("r2")
print(rows)
try:
rows.append("r3")
except AttributeError as err:
print("AttributeError:", err)Output:
None AttributeError: 'NoneType' object has no attribute 'append'
+= changes the list, + makes a new one
For lists, a += b extends a in place, the same as a.extend(b), and every alias sees the new items. a = a + b builds a new list and points a at it, so aliases keep the old list. The augmented assignment you learned on numbers behaves differently here because a list can change in place and an integer cannot.
batch = ["a", "b"]
alias = batch
batch += ["c"]
print(alias)
batch = batch + ["d"]
print(alias)
print(batch)Output:
['a', 'b', 'c'] ['a', 'b', 'c'] ['a', 'b', 'c', 'd']
Choosing a form in a pipeline
- In place: sort(), extend(), +=
- Your code owns the list and no other name needs the old contents
- The list is large and a second copy would double its memory
- The call returns None, so write it as its own statement
- New value: sorted(), +, slicing
- The input must survive, such as a raw batch kept for auditing
- A caller passed the list in and didn't ask for changes
- You want the result back as the return value of the call
Predict the output.
data = [5, 2, 8]
a = data
b = sorted(data)
data.reverse()
print(a, b)- [5, 2, 8] [2, 5, 8]
- [8, 2, 5] [2, 5, 8]
- [8, 2, 5] [8, 5, 2]
- None [2, 5, 8]
Capstone: a plan with undo
The last problems use everything from the earlier sections. You'll see shallow and deep copies, including one made by a comprehension, next to a stack of saved states and calls that either change a list or return a new one. Trace each name to the object it points at before you answer.
What does this program print?
import copy
base = [["load"], ["publish"]]
plan = [steps[:] for steps in base]
plan[0].append("check")
undo = [copy.deepcopy(plan)]
plan[1] = plan[1] + ["notify"]
plan = undo.pop()
print(base, plan)- [['load'], ['publish']] [['load', 'check'], ['publish', 'notify']]
- [['load', 'check'], ['publish']] [['load', 'check'], ['publish']]
- [['load'], ['publish']] [['load', 'check'], ['publish']]
- [['load'], ['publish']] [['load'], ['publish']]
The in-place rule bites hardest when you sort nested data. row.sort() reorders whichever list row names, so if a working copy shares its inner lists with the source, sorting the copy's rows reorders the source's rows too. The copy has to reach the inner lists, the 2-level pattern from the deep copy section, before any in-place call runs.
About this lesson
This section is part of the Lists: Advanced lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.