Lists
list in Python 3.14: creating, indexing and slicing, every method, slice assignment, searching,
sorting, copying and aliasing, costs, the idioms DS&A solutions are built from, typing, and when
another container fits better. Comprehensions are in
Fundamentals; the TypeScript side is in
Arrays.
Creating
| Expression | Result | Notes |
|---|---|---|
[], list() | [] | empty; the literal is a little faster |
[1, "a", None] | mixed items are allowed | annotate: mypy infers list[object], pyright list[int | str | None] |
list("abc") | ['a', 'b', 'c'] | list(iterable) drains anything iterable |
list(range(5)), list(range(10, 0, -3)) | [0, 1, 2, 3, 4], [10, 7, 4, 1] | range alone is a lazy sequence; keep it lazy if you only loop |
list({"a": 1, "b": 2}) | ['a', 'b'] | a dict iterates its keys; list(d.items()) for pairs |
"a b c".split() | ['a', 'b', 'c'] | from text; see Strings |
[0] * 3 | [0, 0, 0] | repeats references: safe only for immutable items |
[[] for _ in range(3)] | [[], [], []] | three separate inner lists |
[*a, *b], [*range(3), 9] | concatenation | unpacks any iterables, not just lists |
[x * 2 for x in xs if x] | comprehension | map + filter in one expression |
list(map(f, xs)), list(gen()) | materialize an iterator | map, filter, zip and generators are lazy |
sorted(s) | new sorted list | from any iterable: set, dict, generator |
grid = [[0] * 3 for _ in range(2)] # 2 rows, 3 columns
grid[0][0] = 1
print(grid) # [[1, 0, 0], [0, 0, 0]]
bad = [[0] * 3] * 2 # the same row object, twice
bad[0][0] = 1
print(bad) # [[1, 0, 0], [1, 0, 0]]Indexing & slicing
xs[start:stop:step]: stop is excluded, negatives count from the end, and a slice clamps
out-of-range bounds and always returns a new list. A single index out of range raises IndexError;
there is no undefined.
| Expression | list("python") gives | JS equivalent |
|---|---|---|
xs[0], xs[-1] | 'p', 'n' | xs[0], xs.at(-1) |
xs[1:4] | ['y', 't', 'h'] | xs.slice(1, 4) |
xs[:2], xs[2:] | ['p', 'y'], ['t', 'h', 'o', 'n'] | xs.slice(0, 2), xs.slice(2) |
xs[-3:], xs[:-1] | ['h', 'o', 'n'], ['p', 'y', 't', 'h', 'o'] | xs.slice(-3), xs.slice(0, -1) |
xs[::2] | ['p', 't', 'o'] | none |
xs[::-1] | ['n', 'o', 'h', 't', 'y', 'p'] | xs.toReversed() |
xs[3:0:-1] | ['h', 't', 'y'] | none: with a negative step, start is included and stop excluded |
xs[:] | shallow copy | xs.slice(), [...xs] |
xs[10:] | [] | xs.slice(10) |
xs[10] | IndexError | undefined |
xs = [10, 20, 30, 40, 50]
evens = slice(None, None, 2) # a reusable, named slice
xs[evens] # [10, 30, 50]
evens.indices(len(xs)) # (0, 5, 2): resolved bounds
first, *rest = xs # 10 and [20, 30, 40, 50]
*init, last = xs # [10, 20, 30, 40] and 50
head = xs[0] if xs else None # no xs.get(i, default)The starred name is always a list, even when unpacking a tuple or a generator.
Adding, removing & replacing
| Operation | Returns | Notes |
|---|---|---|
xs.append(x) | None | one item at the end; appending a list nests it |
xs.extend(it), xs += it | None | every item of any iterable: xs += "ab" adds 'a', 'b' |
xs.insert(i, x) | None | before index i; negatives count from the end, past the end appends |
xs[i] = x | replace one; IndexError past the end (no JS-style auto-grow) | |
xs[a:b] = items | replace a range with any number of items | |
xs[i:i] = items | insert several at i (JS splice(i, 0, ...items)) | |
xs[::2] = items | extended slice: items must have the same length, else ValueError | |
xs.pop(), xs.pop(i) | the removed item | IndexError when empty or out of range |
xs.remove(x) | None | first item equal to x; ValueError if absent |
del xs[i], del xs[a:b], del xs[::2] | delete by index or slice | |
xs.clear(), del xs[:] | None | empty in place: every name for this list sees it |
xs + ys | new list | both must be lists: [1] + (2,) is a TypeError, xs += (2,) is fine |
xs * n, n * xs | new list | n <= 0 gives [] |
xs = ["a", "d"]
xs.insert(1, "b") # ['a', 'b', 'd']
xs[2:2] = ["c"] # insert at 2: ['a', 'b', 'c', 'd']
xs[1:3] = ["X"] # 2 items become 1: ['a', 'X', 'd']
xs[len(xs) :] = ["e", "f"] # same as extend
del xs[-2:] # ['a', 'X', 'd']
xs += "pq" # ['a', 'X', 'd', 'p', 'q']
xs.pop() # 'q'
xs.remove("X") # ['a', 'd', 'p']
ys = [0, 1, 2, 3, 4, 5]
ys[::2] = [9, 9, 9] # [9, 1, 9, 3, 9, 5]
del ys[1::2] # [9, 9, 9]
zs: list[object] = [1, 2]
zs.append([3, 4]) # nests: [1, 2, [3, 4]]
zs.extend([3, 4]) # [1, 2, [3, 4], 3, 4]1/4
xs = [10, 20, 30, 40, 50] is built from a literal with room for 6 pointers, so slot [5] is spare.
Searching & testing
| Expression | Returns | Notes |
|---|---|---|
x in xs, x not in xs | bool | linear scan; checks identity before == |
xs.index(x) | first index | ValueError if absent: there is no -1 |
xs.index(x, start, stop) | first index in xs[start:stop] | counted from the start of xs |
xs.count(x) | number of equal items | |
len(xs) | length | stored, so O(1) |
min(xs), max(xs) | smallest / largest | key=; ValueError on empty unless default= |
sum(xs) | total | math.fsum for accurate floats; sum refuses strings |
any(xs), all(xs) | bool | stop early; any([]) is False, all([]) is True |
next((x for x in xs if p(x)), None) | first match or the default | JS find |
bisect.bisect_left(xs, x) | insertion point | O(log n), sorted lists only |
xs == ys | bool | same length and items equal pairwise; [1, 2] == (1, 2) is False |
xs < ys | bool | lexicographic: first differing item decides, else the shorter list is smaller |
xs = [3, 1, 4, 1, 5, 9, 2, 6]
4 in xs # True
xs.index(1), xs.index(1, 2) # (1, 3)
xs.count(1) # 2
max(xs), min(xs, key=lambda n: abs(n - 7)) # (9, 6)
max([], default=None) # None
next((n for n in xs if n > 4), None) # 5
max(range(len(xs)), key=xs.__getitem__) # 5: argmax
[i for i, n in enumerate(xs) if n == 1] # [1, 3]
[1, 2] < [1, 2, 0], [2] > [1, 99] # (True, True)Sorting & reversing
| Call | Returns | Mutates | Notes |
|---|---|---|---|
xs.sort(key=None, reverse=False) | None | yes | lists only; arguments are keyword-only |
sorted(it, key=None, reverse=False) | new list | no | any iterable: tuple, set, dict keys, generator |
xs.reverse() | None | yes | in place |
reversed(xs) | iterator | no | lazy; list(reversed(xs)) for a copy |
xs[::-1] | new list | no | reversed copy |
random.shuffle(xs) | None | yes | random.sample(xs, k=len(xs)) for a shuffled copy |
Rules that differ from JS:
- The default order uses
<on the items: numbers sort numerically ([10, 9, 1]gives[1, 9, 10]), strings by code point ("Z" < "a"). MixingintandstrraisesTypeError. - There is no compare function.
key=fcallsfonce per item and sorts by the results; tuples as keys give multi-key sorts.functools.cmp_to_key(cmp)adapts a JS-style(a, b) -> int. - The sort is stable (equal keys keep their order), including with
reverse=True. - Timsort: O(n log n), and close to O(n) on data that is already mostly in order.
from operator import attrgetter, itemgetter
words = ["banana", "Cherry", "apple", "fig"]
sorted(words) # ['Cherry', 'apple', 'banana', 'fig']
sorted(words, key=str.casefold)
# ['apple', 'banana', 'Cherry', 'fig']
sorted(words, key=len, reverse=True) # stable ties
# ['banana', 'Cherry', 'apple', 'fig']
rows = [("ann", 31), ("bo", 25), ("cy", 31)]
sorted(rows, key=itemgetter(1)) # by age
# [('bo', 25), ('ann', 31), ('cy', 31)]
sorted(rows, key=lambda r: (-r[1], r[0])) # age desc, name
# [('ann', 31), ('cy', 31), ('bo', 25)]
nums = [3, None, 1]
sorted(nums, key=lambda n: (n is None, n)) # None last
# [1, 3, None]
by = attrgetter("last", "first") # objects: key=by| Key | Sorts by |
|---|---|
key=str.casefold | case-insensitive text |
key=len | length |
key=itemgetter(1), itemgetter("age", "name") | index or dict key; several give a tuple |
key=attrgetter("age") | attribute; dotted paths like "owner.name" work |
key=lambda x: (-x.score, x.name) | score descending, then name ascending |
key=locale.strxfrm | the current locale's collation (after locale.setlocale) |
key=d.__getitem__ | the values of dict d, when sorting its keys (key=d.get runs too, but type checkers reject it: get may return None) |
Iterating & transforming
| Tool | Gives | JS |
|---|---|---|
[f(x) for x in xs if p(x)] | new list | xs.filter(p).map(f) |
map(f, xs), filter(p, xs) | lazy, single-use iterators | xs.map(f), xs.filter(p) (eager arrays) |
filter(None, xs) | truthy items | xs.filter(Boolean) |
enumerate(xs, start=0) | (index, item) pairs | xs.entries() |
zip(a, b, strict=True) | tuples in step; strict raises on unequal lengths | none |
zip(*rows) | columns: a transpose | none |
reversed(xs) | items from the end, no copy | none |
itertools.chain.from_iterable(xss) | flatten one level | xs.flat() |
itertools.pairwise(xs) | (xs[0], xs[1]), (xs[1], xs[2]), ... | none |
itertools.batched(xs, n) | tuples of n; strict=True (3.13+) rejects a short last one | none |
itertools.accumulate(xs) | running totals; initial=0 prepends a 0 | none |
itertools.groupby(xs, key) | runs of equal keys: sort by the key first | Object.groupBy (whole input, not runs) |
functools.reduce(f, xs, init) | a fold | xs.reduce(f, init) |
from itertools import accumulate, pairwise
names = ["ann", "bo", "cy"]
scores = [90, 72, 85]
for rank, name in enumerate(names, start=1):
print(rank, name) # 1 ann, 2 bo, 3 cy
passed = [n for n, s in zip(names, scores) if s >= 80]
# ['ann', 'cy']
by_name = dict(zip(names, scores, strict=True))
# {'ann': 90, 'bo': 72, 'cy': 85}
list(accumulate([1, 2, 3, 4])) # [1, 3, 6, 10]
[b - a for a, b in pairwise([1, 4, 9, 16])] # [3, 5, 7]Removing items from a list while looping over it skips items, because the loop walks by index:
xs = [1, 2, 2, 3]
for x in xs:
if x == 2:
xs.remove(x) # ❌ skips the item after x
print(xs) # [1, 2, 3]
xs = [1, 2, 2, 3]
xs[:] = [x for x in xs if x != 2] # ✅ [1, 3], same list
for x in xs.copy(): # ✅ or loop over a copy
if x == 1:
xs.remove(x)Copying & aliasing
Assignment binds a name; it never copies. A function that receives a list receives the caller's list.
| Expression | Gives | Depth |
|---|---|---|
ys = xs | the same list under a second name | none |
xs.copy(), xs[:], list(xs), [*xs] | a new list holding the same items | shallow |
copy.deepcopy(xs) | a new list with items copied recursively | deep |
xs.clear() vs xs = [] | empties the list for everyone vs rebinds only xs | |
xs += ys vs xs = xs + ys | extends in place vs builds a new list |
import copy
a = [[1, 2], [3]]
b = a # alias
c = a.copy() # new outer list, same inner lists
d = copy.deepcopy(a) # nothing shared
a[0].append(99)
a.append([4])
print(b) # [[1, 2, 99], [3], [4]]
print(c) # [[1, 2, 99], [3]]
print(d) # [[1, 2], [3]]
xs = [1]
ys = xs
xs += [2] # in place: ys is [1, 2]
xs = xs + [3] # new list: ys stays [1, 2]
def tag(items: list[str]) -> None:
items.append("seen") # mutates the caller's list
names = ["ann"]
tag(names) # names is ['ann', 'seen']1/7
a = [[1, 2], [3]] builds an outer list whose two slots point to two inner lists.
Costs & memory
CPython's list is a dynamic array of pointers to objects. When it is full it reallocates with about
12.5% spare room, so appends are amortized O(1); anything that inserts or deletes near the front
moves every later pointer.
1/8
xs = [] has length 0 and no pointer array yet: sys.getsizeof(xs) is 56, the header alone.
| Operation | Cost | Notes |
|---|---|---|
xs[i], xs[i] = x, len(xs) | O(1) | |
xs.append(x), xs.pop() | O(1) amortized | the end is cheap |
xs.insert(i, x), xs.pop(i), del xs[i] | O(n − i) | items after i shift |
xs.insert(0, x), xs.pop(0) | O(n) | collections.deque does both ends in O(1) |
x in xs, index, count, remove | O(n) | a set or dict answers in in O(1) |
xs[a:b], xs.copy() | O(b − a), O(n) | copies the pointers |
xs.extend(ys), xs += ys | O(len(ys)) | |
xs + ys | O(len(xs) + len(ys)) | xs = xs + [x] in a loop is O(n²) overall |
xs.sort(), sorted(xs) | O(n log n) | |
min, max, sum, xs.reverse() | O(n) | |
bisect.bisect_left / insort | O(log n) / O(n) | the search is fast, the insert still shifts |
heapq.heappush / heappop | O(log n) | a priority queue on a plain list |
import sys
from array import array
sys.getsizeof([]) # 56: header only (64-bit CPython)
sys.getsizeof([None] * 3) # 80: plus 3 pointers of 8 bytes
sys.getsizeof(1.0) # 24: each float is its own object
prices = array("d", [1.5, 2.25]) # packed C doubles
prices.append(3.0) # 8 bytes per item, no objectsgetsizeof never counts the items themselves. Flattening lists with sum(lists, []) is quadratic
for the same reason as xs = xs + [x]; use itertools.chain.from_iterable or a comprehension.
| Need | Use instead | Why |
|---|---|---|
| a fixed record, a dict key or set member | tuple | immutable, and hashable when its items are; see Tuples |
| a queue, or adding/removing at both ends | collections.deque | O(1) appendleft/popleft; maxlen= makes a ring buffer |
| fast membership, uniqueness | set, dict.fromkeys(xs) | O(1) average in; dict.fromkeys keeps order; see Sets |
| lookup by key | dict | O(1) average by key instead of a scan |
| the smallest item, repeatedly | heapq on a list | O(log n) push and pop; 3.14 adds heappush_max and friends |
| top n of many | heapq.nlargest(n, xs, key=) | no full sort |
| millions of numbers | array.array or a NumPy ndarray | packed values instead of pointers to objects |
| a read-only view for callers | return a tuple, or type it Sequence[T] | callers can't mutate your list |
| a list that validates or reacts to writes | subclass collections.UserList | a list subclass's extend, insert and += never call your overridden append or __setitem__ |
DS&A idioms
The list moves that problem solutions are built from, and the trap in each. The patterns themselves, with TypeScript and JavaScript beside Python, are in the DS&A sheets (linked in the last column).
| Job | Python | Watch out | More |
|---|---|---|---|
| Stack | st.append(x), st.pop(), peek st[-1], empty check if st: | pop() and st[-1] raise IndexError on an empty stack | Stacks |
| Queue, BFS | q = deque([start]), q.append(x), q.popleft() | never xs.pop(0): O(n) per pop turns BFS into O(n²) | Queues & deques |
| Min-heap | heappush(h, (prio, i, item)), heappop(h), peek h[0] | tuples compare field by field: on a prio tie the next field decides, so add a counter i before items that can't compare | Heaps, heapq |
| Max-heap | heapify_max, heappush_max, heappop_max (3.14) | before 3.14: push -x, negate again on pop | Max-heaps, heapq |
| Top k | heapq.nlargest(k, xs, key=f), nsmallest | O(n log k); max/min for k = 1 | Top-k, heapq |
| Sorted lookup | i = bisect_left(xs, x): first xs[i] >= x; bisect_right: first > x | i can be len(xs): test i < len(xs) and xs[i] == x before reading | Lower & upper bound |
| Search the answer | bisect_left(range(lo, hi), True, key=ok) | ok must be monotonic (all False, then all True); the result i is an offset, so the answer is lo + i, and i == hi - lo means none works | On the answer |
| Prefix sums | pre = [0, *accumulate(xs)], then sum(xs[i:j]) == pre[j] - pre[i] | the leading 0 makes ranges starting at 0 work | Prefix sums |
| Two pointers | i, j = 0, len(xs) - 1, while i < j:, swap xs[i], xs[j] = xs[j], xs[i] | the right side is built first, then targets are assigned left to right | Two pointers |
| DP table | dp = [0] * (n + 1), dp = [[0] * (m + 1) for _ in range(n + 1)] | [[0] * m] * n shares one row between all n | Bottom-up DP |
| Sentinels | dist = [math.inf] * n, best = -math.inf | math.inf is a float: int(math.inf) raises OverflowError | Numbers |
| Counting | cnt = [0] * 26, cnt[ord(ch) - ord("a")] += 1, or Counter(s) | lists are unhashable: key a dict by tuple(cnt) | Group anagrams, dict idioms |
| Grid neighbors | for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):, then 0 <= r < R and 0 <= c < C | negative indexes don't raise: grid[-1] silently reads the last row | Grids as graphs |
| Intervals | ivs.sort() sorts by start, then end | key=lambda iv: iv[0] when ties must keep input order | Intervals |
| Argsort | order = sorted(range(len(xs)), key=xs.__getitem__) | indexes in value order; stable for ties | Argsort |
| Backtracking | path.append(x), recurse, path.pop(); record out.append(path[:]) | out.append(path) records the same list every time, and it ends up empty | Backtracking |
| Memo keys | @cache on ints, strings, tuples | a list argument raises TypeError: unhashable; pass tuple(xs) or an index | |
| Deep recursion | an explicit stack: st = [root], while st: | the limit is 1,000 frames by default (sys.getrecursionlimit()) | Depth limits, Graphs |
import heapq
from bisect import bisect_left, bisect_right
from itertools import accumulate
jobs = [(2, "b"), (1, "a"), (2, "c")]
h: list[tuple[int, int, str]] = []
for i, (prio, name) in enumerate(jobs):
heapq.heappush(h, (prio, i, name)) # i breaks ties
heapq.heappop(h) # (1, 1, 'a')
xs = [1, 3, 3, 3, 7]
bisect_left(xs, 3), bisect_right(xs, 3) # (1, 4)
bisect_right(xs, 3) - bisect_left(xs, 3) # 3 copies of 3
bisect_left(range(100), True, key=lambda m: m * m >= 50)
# 8: the smallest m with m * m >= 50
pre = [0, *accumulate([2, 4, 1, 5])] # [0, 2, 6, 7, 12]
pre[3] - pre[1] # 5: items 1 and 2 summed in O(1)
grid = ["..#", "...", "#.."]
R, C = len(grid), len(grid[0])
r, c = 0, 0
nbrs = [
(r + dr, c + dc)
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1))
if 0 <= r + dr < R and 0 <= c + dc < C
]
print(nbrs) # [(1, 0), (0, 1)]: no wrap to row -1
def subsets(nums: list[int]) -> list[list[int]]:
out: list[list[int]] = []
path: list[int] = []
def go(i: int) -> None:
if i == len(nums):
out.append(path[:]) # copy: path keeps changing
return
go(i + 1) # skip nums[i]
path.append(nums[i]) # take nums[i]
go(i + 1)
path.pop() # undo
go(0)
return out
print(subsets([1, 2])) # [[], [2], [1], [1, 2]]Typing
| Annotation | Accepts | Use for |
|---|---|---|
list[T] | lists only | values you build, mutate or return |
Sequence[T] | list, tuple, str, range, ... | read-only parameters that index or call len |
Iterable[T] | anything a for loop takes | parameters you only loop over once |
MutableSequence[T] | list, deque, array, UserList | parameters you edit in place without demanding a list |
tuple[T, ...] | tuples of any length | immutable results |
typing.List is a deprecated alias; write list[int]. Import the abstract types from
collections.abc.
from collections.abc import Iterable, Sequence
def mean(xs: Sequence[float]) -> float: # read-only
return sum(xs) / len(xs)
def add_all(dst: list[int], src: Iterable[int]) -> None:
dst.extend(src) # needs a real list
ints: list[int] = [1, 2]
mean(ints) # OK: Sequence is covariant
nums: list[float] = ints # ❌ type error: list is invariant
empty: list[str] = [] # annotate empty lists
maybe: list[int | None] = [1, None]
type Matrix = list[list[float]] # alias (3.12+)
def first[T](xs: Sequence[T]) -> T | None: # 3.12+
return xs[0] if xs else Nonelist is invariant because it is writable: if list[int] were accepted as list[float], the
callee could append 0.5 to your list of ints. Sequence has no mutating methods, so it is
covariant and accepts list[int] where Sequence[float] is expected.
Coming from JavaScript
The TypeScript version of this sheet is Arrays.
JS Array | Python list |
|---|---|
xs.length | len(xs) |
xs.push(x), xs.push(...ys) | xs.append(x), xs.extend(ys) |
xs.pop() | xs.pop(): raises IndexError on empty instead of undefined |
xs.shift(), xs.unshift(x) | xs.pop(0), xs.insert(0, x): O(n), so a deque for queues |
xs.splice(i, n) | del xs[i : i + n], or slice it first to keep the removed items |
xs.splice(i, 0, a, b) | xs[i:i] = [a, b] |
xs.slice(a, b), xs.at(-1) | xs[a:b], xs[-1] |
xs.concat(ys), [...xs, ...ys] | xs + ys, [*xs, *ys] |
xs.indexOf(x) | xs.index(x): raises ValueError, no -1 |
xs.includes(x) | x in xs |
xs.find(p), xs.findIndex(p) | next((x for x in xs if p(x)), None), next((i for i, x in enumerate(xs) if p(x)), -1) |
xs.findLast(p) | next((x for x in reversed(xs) if p(x)), None) |
xs.map(f), xs.filter(p) | [f(x) for x in xs], [x for x in xs if p(x)] |
xs.flat(), xs.flatMap(f) | list(chain.from_iterable(xs)), [y for x in xs for y in f(x)] |
xs.reduce(f, init) | functools.reduce(f, xs, init); usually sum, max, min |
xs.some(p), xs.every(p) | any(p(x) for x in xs), all(p(x) for x in xs) |
xs.join(", ") | ", ".join(xs): items must be str, so ", ".join(map(str, xs)) |
xs.sort((a, b) => a - b) | xs.sort(): numbers already sort numerically |
xs.toSorted(cmp), xs.toReversed() | sorted(xs, key=...), xs[::-1] |
xs.with(i, v) | ys = xs.copy() then ys[i] = v |
xs.forEach((x, i) => ...) | for i, x in enumerate(xs): |
new Array(n).fill(0) | [0] * n |
Array.from({ length: n }, (_, i) => i) | list(range(n)) |
Array.isArray(x) | isinstance(x, list) |
xs.length = 0 | xs.clear() |
[...new Set(xs)] | list(dict.fromkeys(xs)) (keeps first-seen order) |
structuredClone(xs) | copy.deepcopy(xs) |
Recipes
Flatten, chunk, dedupe and group-by recipes are in Fundamentals.
Sliding window
When you need every run of k consecutive items: moving averages, n-grams. The variable-size,
two-pointer window is in Sliding window.
from collections import deque
from collections.abc import Iterable, Iterator
from itertools import islice
def windows[T](
items: Iterable[T], k: int
) -> Iterator[tuple[T, ...]]:
it = iter(items)
win = deque(islice(it, k), maxlen=k)
if len(win) == k:
yield tuple(win)
for x in it:
win.append(x) # maxlen drops the oldest
yield tuple(win)
prices = [10, 11, 13, 12, 15]
print([sum(w) / 3 for w in windows(prices, 3)])
# [11.333333333333334, 12.0, 13.333333333333334]Partition by a predicate
When you want the matches and the rest in one pass.
from collections.abc import Callable, Iterable
def partition[T](
items: Iterable[T], pred: Callable[[T], bool]
) -> tuple[list[T], list[T]]:
yes: list[T] = []
no: list[T] = []
for x in items:
(yes if pred(x) else no).append(x)
return yes, no
evens, odds = partition(range(7), lambda n: n % 2 == 0)
print(evens, odds) # [0, 2, 4, 6] [1, 3, 5]Sort by several keys, mixed directions
When some keys run ascending and others descending, and not every key is a number you can negate.
from dataclasses import dataclass
from operator import attrgetter
@dataclass
class Player:
team: str
score: int
name: str
players = [
Player("red", 10, "cy"),
Player("blue", 10, "ann"),
Player("red", 12, "bo"),
]
# numeric keys can be negated: team, score desc, name
players.sort(key=lambda p: (p.team, -p.score, p.name))
# strings can't: sort by the minor key first, then the
# major one; stability keeps the minor order in ties
players.sort(key=attrgetter("name"), reverse=True)
players.sort(key=attrgetter("team"))
print([(p.team, p.name) for p in players])
# [('blue', 'ann'), ('red', 'cy'), ('red', 'bo')]Natural sort
When file2 should come before file10.
import re
def natural_key(s: str) -> list[int | str]:
parts = re.split(r"(\d+)", s.casefold())
return [int(p) if p.isdecimal() else p for p in parts]
files = ["file10.txt", "File2.txt", "file1.txt"]
print(sorted(files, key=natural_key))
# ['file1.txt', 'File2.txt', 'file10.txt']The split alternates text and digits, so items at the same position always have the same type and
never compare int with str. isdecimal() matches exactly what \d captured; isdigit() would
also accept "²", which int() rejects.
Keep a list sorted
When items arrive one at a time and you need them in order, with binary search.
import bisect
from dataclasses import dataclass
@dataclass
class Job:
due: int
name: str
queue: list[Job] = []
for job in [Job(5, "b"), Job(1, "a"), Job(9, "c")]:
bisect.insort(queue, job, key=lambda j: j.due)
print([j.name for j in queue]) # ['a', 'b', 'c']
i = bisect.bisect_left(queue, 5, key=lambda j: j.due)
print(queue[i].name) # binsort applies key to the new item; bisect_left and bisect_right do not, so pass them the
key value (5), not a Job.
Move, rotate and swap
When reordering after a drag and drop, or cycling through a list.
def move[T](xs: list[T], src: int, dst: int) -> None:
xs.insert(dst, xs.pop(src))
def rotate[T](xs: list[T], k: int) -> list[T]:
if not xs:
return []
k %= len(xs) # also handles k < 0 and k > len
return xs[-k:] + xs[:-k]
tasks = ["a", "b", "c", "d"]
move(tasks, 0, 2)
print(tasks) # ['b', 'c', 'a', 'd']
print(rotate([1, 2, 3, 4, 5], 2)) # [4, 5, 1, 2, 3]
tasks[0], tasks[-1] = tasks[-1], tasks[0] # swap
print(tasks) # ['d', 'c', 'a', 'b']collections.deque.rotate(k) does the same rotation in place.
Transpose and rotate a grid
When working with a matrix stored as a list of rows.
m = [[1, 2, 3], [4, 5, 6]]
print([list(col) for col in zip(*m)])
# [[1, 4], [2, 5], [3, 6]]
print([list(row) for row in zip(*m[::-1])]) # 90° right
# [[4, 1], [5, 2], [6, 3]]
print([list(row) for row in zip(*m)][::-1]) # 90° left
# [[3, 6], [2, 5], [1, 4]]zip stops at the shortest row; ragged grids need itertools.zip_longest(*m, fillvalue=None).
Diff two lists
When syncing: what was added and what was removed, in order.
from collections import Counter
old = ["a", "b", "c", "d"]
new = ["b", "d", "e"]
old_set, new_set = set(old), set(new)
added = [x for x in new if x not in old_set] # ['e']
removed = [x for x in old if x not in new_set]
print(added, removed) # ['e'] ['a', 'c']
# with duplicates: multiset difference
extra = Counter([1, 1, 2, 3]) - Counter([1, 3])
print(sorted(extra.elements())) # [1, 2]References
- Python docs: More on lists (opens in a new tab): every method, with examples
- Python docs: Sequence types (opens in a new tab): common and mutable sequence operations, slicing rules,
list.sort - Python docs: Sorting techniques (opens in a new tab): key functions, stability, multi-pass sorts,
cmp_to_key - Python docs: bisect (opens in a new tab) and heapq (opens in a new tab): sorted lists and priority queues
- Python docs: itertools (opens in a new tab):
pairwise,batched,accumulate,groupby, recipes - Python docs: copy (opens in a new tab): shallow and deep copies
- Python docs: collections (opens in a new tab) and collections.abc (opens in a new tab):
deque,UserList,Sequence,MutableSequence - Python FAQ: How are lists implemented in CPython? (opens in a new tab)
- Python FAQ: Why did changing list 'y' also change list 'x'? (opens in a new tab)
- Python FAQ: Why does
a_tuple[i] += ['item']raise an exception when the addition works? (opens in a new tab) - Python wiki: Time complexity (opens in a new tab): costs of
list,deque,setanddictoperations - Typing docs: Variance of generic types (opens in a new tab): why
listis invariant andSequencecovariant