Python – Lists & List Comprehensions

July 14, 20265 min readUpdated 8/20/2026

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.538400.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.