The list is Python's default container: ordered, changeable, and happy to hold anything. This post covers what you actually do with one, then comprehensions — the syntax that replaces most of the loops you would otherwise write around them.
Making and reading a list
accounts = ["Checking", "Savings", "Credit"]
print(accounts[0]) # Output: Checking
print(accounts[-1]) # Output: Credit
print(accounts[1:]) # Output: ['Savings', 'Credit']
print(len(accounts)) # Output: 3
print("Savings" in accounts) # Output: True
Indexes start at zero and negative ones count from the end, so [-1] is the last item —
no len(x) - 1 arithmetic. Slicing follows the same half-open rule as everywhere else, and
a slice returns a new list.
A list can hold mixed types. That is legal and occasionally useful, but a list whose items are all different things is usually better as a tuple or an object.
Changing one
accounts = ["Checking", "Savings"]
accounts.append("Credit") # add one to the end
accounts.insert(0, "Offset") # add at a position
accounts.extend(["ISA", "Bond"]) # add several
accounts.remove("Savings") # delete by value
last = accounts.pop() # remove and return the last
print(accounts) # Output: ['Offset', 'Checking', 'Credit', 'ISA']
print(last) # Output: Bond
These all modify the list in place and return None — which is why
accounts = accounts.append("x") is a bug that leaves you holding None. It is
the mirror image of the string rule: strings return a new value and lists change themselves.
append adds one item; extend adds each item of another collection.
append a list and you get a list nested inside your list.
Sorting
balances = [1250.00, 8400.50, 320.75]
print(sorted(balances)) # Output: [320.75, 1250.0, 8400.5]
print(sorted(balances, reverse=True)) # Output: [8400.5, 1250.0, 320.75]
print(balances) # Output: [1250.0, 8400.5, 320.75]
balances.sort()
print(balances) # Output: [320.75, 1250.0, 8400.5]
Two different tools with almost the same name. sorted(x) returns a new list and leaves
the original alone; x.sort() rearranges the list itself and returns None.
The third line proves the original survived the sorted() calls.
To sort by something other than the value itself, pass a key:
accounts = [
{"type": "Savings", "balance": 8400.50},
{"type": "Checking", "balance": 1250.00},
]
for account in sorted(accounts, key=lambda a: a["balance"]):
print(account["type"])
# Output: Checking
# Output: Savings
The key function is called once per item and the results are what get compared. This is
the single most useful thing to know about sorting in Python, and it is why
lambdas exist.
Copying — the one that causes real bugs
Assignment does not copy. It gives the same list a second name:
original = ["Checking", "Savings"]
alias = original # same list
copy = original.copy() # a new list
alias.append("Credit")
copy.append("ISA")
print(original) # Output: ['Checking', 'Savings', 'Credit']
print(copy) # Output: ['Checking', 'Savings', 'ISA']
original saw the append through alias and did not see the one through
copy. Use .copy(), list(x) or x[:] when you mean a
copy — they are equivalent.
All three are shallow: the new list holds the same objects. For a list of lists,
use copy.deepcopy().
List comprehensions
A comprehension builds a list from another collection in one expression. This loop:
balances = [1250.00, 8400.50, 320.75]
with_interest = []
for balance in balances:
with_interest.append(round(balance * 1.05, 2))
print(with_interest) # Output: [1312.5, 8820.52, 336.79]
is this comprehension:
balances = [1250.00, 8400.50, 320.75]
with_interest = [round(b * 1.05, 2) for b in balances]
print(with_interest) # Output: [1312.5, 8820.52, 336.79]
(If you were expecting 8820.53 — 8400.50 * 1.05 is exactly
8820.525, and round() breaks a tie by going to the even digit rather
than always up. That is deliberate, it is why you use Decimal for money, and
Data Types covers it.)
Read it left to right: what to collect, then where it comes from. The three lines
of setup-and-append vanish, and — more usefully — the reader can see at a glance that this builds one
list from another and does nothing else. A for loop might do anything.
Filtering, and the conditional form
amounts = [100, -25, 250, -40, 0]
print([a for a in amounts if a > 0]) # Output: [100, 250]
print([abs(a) for a in amounts if a < 0]) # Output: [25, 40]
print(["credit" if a > 0 else "debit" for a in amounts])
# Output: ['credit', 'debit', 'credit', 'debit', 'debit']
An if at the end filters — items that fail it are left out. An
if/else at the front chooses a value and keeps every item. Two different
jobs that look similar; the position tells you which one you are reading.
The functions that consume a whole list
Several builtins take a list and give you one answer. Reaching for these instead of writing the loop is most of what "idiomatic Python" means in practice:
balances = [1250.00, 8400.50, 320.75]
print(sum(balances)) # Output: 9971.25
print(min(balances), max(balances)) # Output: 320.75 8400.5
print(len(balances)) # Output: 3
print(any(b < 0 for b in balances)) # Output: False
print(all(b > 0 for b in balances)) # Output: True
any() and all() are the two people forget. "Is any account overdrawn"
is one line, it reads as the sentence you would say out loud, and it stops at the first answer rather
than checking the rest.
Note there are no brackets inside those last two calls — that is a generator expression, not a list, so nothing is built in memory just to be counted.
When to stop
Comprehensions get unreadable faster than loops do. The line to hold:
rows = [["a", "b"], ["c", "d"]]
flat = [cell for row in rows for cell in row]
print(flat) # Output: ['a', 'b', 'c', 'd']
Two for clauses is the flatten idiom, and it is worth memorising because the order
reads backwards from what you would expect — outer loop first, exactly as if you had nested them.
Past that, use a loop. Specifically: more than two clauses, or a condition that needs its own thought, means write the loop. A comprehension is for when the transformation is obvious; when it is not, the loop's extra lines buy you somewhere to put a name.
The same syntax builds dicts and sets, and swapping the brackets for parentheses gives you a generator that does not build anything at all.
Next: Tuples.