Creating lists
A list is an ordered, mutable collection. You can put anything in a list: numbers, strings, booleans, other lists, even functions. Items don't have to be the same type.
1# Literal syntax2fruits = ["apple", "banana", "cherry"]3nums = [1, 2, 3, 4, 5]4empty = []56# Mixed types are fine7mixed = [1, "hello", True, None, 3.14]89# Nested lists10matrix = [11 [1, 2, 3],12 [4, 5, 6],13 [7, 8, 9],14]15print(matrix[1][2]) # 6: row 1, column 2
list() constructor
Convert any iterable to a list.
1list("hello") # ['h', 'e', 'l', 'l', 'o']2list(range(5)) # [0, 1, 2, 3, 4]3list((1, 2, 3)) # [1, 2, 3]: tuple to list4list({3, 1, 2}) # [1, 2, 3]: set to list (order varies)56# range() is common for generating sequences7evens = list(range(0, 10, 2))8print(evens) # [0, 2, 4, 6, 8]
Initializing with a default value
The * operator repeats a list. Use it to pre-fill a list with a known size. Works for immutables only. See the gotcha below.
1zeros = [0] * 52print(zeros) # [0, 0, 0, 0, 0]34flags = [False] * 35print(flags) # [False, False, False]
Gotcha: [[]] * 3 creates three references to the same inner list, not three separate lists. Modifying one modifies all. Use a comprehension instead: [[] for _ in range(3)].
Indexing and slicing
Lists are sequences, so everything from the Strings article applies: positive indices count from the left, negative from the right, and slicing extracts a sublist.
1items = ["a", "b", "c", "d", "e"]2# 0 1 2 3 43# -5 -4 -3 -2 -145print(items[0]) # "a": first6print(items[-1]) # "e": last7print(items[-2]) # "d": second from last89print(items[1:3]) # ["b", "c"]: indices 1 and 210print(items[:2]) # ["a", "b"]: first two11print(items[2:]) # ["c", "d", "e"]: from index 2 onward12print(items[::-1]) # ["e", "d", "c", "b", "a"]: reversed
Slice assignment
Unlike strings, lists are mutable. You can assign directly to an index or a slice, which modifies the list in place.
1items = [1, 2, 3, 4, 5]23# Replace a single element4items[0] = 105print(items) # [10, 2, 3, 4, 5]67# Replace a slice: replacement can be a different length8items[1:3] = [20, 30, 40]9print(items) # [10, 20, 30, 40, 4, 5]1011# Delete a slice12del items[1:3]13print(items) # [10, 40, 4, 5]1415# Clear via slice16items[:] = []17print(items) # []
Nested lists
1grid = [2 [1, 2, 3],3 [4, 5, 6],4 [7, 8, 9],5]67# Access: row first, then column8print(grid[0][0]) # 1: top-left9print(grid[2][2]) # 9: bottom-right1011# Iterate rows12for row in grid:13 print(row)1415# Get a column (list comprehension)16col_1 = [row[1] for row in grid]17print(col_1) # [2, 5, 8]
Modifying lists
Lists are mutable: you can add, remove, and rearrange items after creation. Try the operations below, then read the explanations.
['apple', 'banana', 'cherry', 'date']Adding items
1fruits = ["apple", "banana"]23# append(): add one item to the end, O(1), fast4fruits.append("cherry")5print(fruits) # ["apple", "banana", "cherry"]67# insert(index, value): add at a specific position, O(n), shifts items right8fruits.insert(1, "avocado")9print(fruits) # ["apple", "avocado", "banana", "cherry"]1011# extend(): add all items from another iterable12fruits.extend(["date", "elderberry"])13print(fruits) # ["apple", "avocado", "banana", "cherry", "date", "elderberry"]1415# + creates a NEW list, doesn't modify in place16more = fruits + ["fig"]17print(more is fruits) # False: different object
append vs extend: fruits.append(["fig", "grape"]) adds the list as a single nested element. fruits.extend(["fig", "grape"]) adds each item individually.
Removing items
1fruits = ["apple", "banana", "cherry", "banana"]23# remove(value): removes FIRST occurrence by value, raises ValueError if missing4fruits.remove("banana")5print(fruits) # ["apple", "cherry", "banana"]67# pop(index): removes and returns item at index (default: last)8last = fruits.pop()9print(last) # "banana"10print(fruits) # ["apple", "cherry"]1112first = fruits.pop(0)13print(first) # "apple"1415# del: remove by index or slice, no return value16items = [1, 2, 3, 4, 5]17del items[1]18print(items) # [1, 3, 4, 5]1920# clear(): remove everything21items.clear()22print(items) # []
Other operations
1nums = [3, 1, 4, 1, 5, 9, 2]23# copy(): shallow copy4copy = nums.copy() # same as nums[:] or list(nums)56# reverse(): in-place7nums.reverse()8print(nums) # [2, 9, 5, 1, 4, 1, 3]910# sort(): in-place11nums.sort()12print(nums) # [1, 1, 2, 3, 4, 5, 9]1314# count(value): how many times value appears15print(nums.count(1)) # 21617# index(value): position of first occurrence18print(nums.index(4)) # 4
List comprehensions
A list comprehension builds a new list from an iterable in a single expression. It is faster than a loop, more readable for simple transformations, and one of the most distinctly Pythonic features in the language.
The speed difference comes from how Python runs them. A regular loop calls .append() on every iteration. Each call is a Python-level method lookup and function invocation. A comprehension skips that entirely. Python handles the whole loop internally in C, using a single optimized bytecode instruction to add each item. The more items you process, the wider the gap.
1# Loop version2squares = []3for x in range(6):4 squares.append(x ** 2)56# Comprehension version: same result, one line7squares = [x ** 2 for x in range(6)]8print(squares) # [0, 1, 4, 9, 16, 25]
Syntax: [expression for item in iterable if condition]. The if condition part is optional.
With a filter condition
1nums = range(20)23evens = [x for x in nums if x % 2 == 0]4print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]56# Transform and filter at the same time7even_squares = [x ** 2 for x in nums if x % 2 == 0]8print(even_squares) # [0, 4, 16, 36, 64, 100, 144, 196, 256, 324]910# Filter a list of strings11words = ["hello", "", "world", " ", "python"]12non_empty = [w for w in words if w.strip()]13print(non_empty) # ['hello', 'world', 'python']
Nested comprehensions
When a comprehension has multiple for clauses, the order trips people up. The rule is simple: write the for clauses in exactly the same order you would write the nested loops, top to bottom. Outer loop first, inner loop second.
1# Flatten a 2D list2matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]3flat = [x for row in matrix for x in row]4print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]56# Cartesian product7colors = ["red", "green"]8sizes = ["S", "M", "L"]9variants = [(c, s) for c in colors for s in sizes]10print(variants)11# [('red','S'),('red','M'),('red','L'),('green','S'),('green','M'),('green','L')]
Formula
Translate stacked loops into a comprehension by reading top to bottom, left to right. The expression you want to collect goes at the front.
Nested loops
for row in matrix: # outer
for x in row: # inner
result.append(x)Comprehension
[x # expression for row in matrix # outer for x in row] # inner
When not to use comprehensions
The readability advantage disappears when the logic gets complex. If you need multiple conditions, nested calls, or side effects, a plain loop communicates intent more clearly. Cleverness is not a virtue here.
1# Too complex, use a regular loop2result = [3 process(item)4 for item in get_items()5 if item.is_valid()6 if item.category in allowed_categories7 if item.price < max_price8]910# Clear loop version11result = []12for item in get_items():13 if not item.is_valid():14 continue15 if item.category not in allowed_categories:16 continue17 if item.price >= max_price:18 continue19 result.append(process(item))
If the comprehension does not fit on one readable line, write a loop. The goal is code that communicates clearly, not the fewest lines.
Sorting
Python gives you two ways to sort. The difference matters: sort() modifies the list in place and returns None. sorted() returns a new sorted list and leaves the original untouched.
1nums = [3, 1, 4, 1, 5, 9, 2]23# sorted(): returns new list, original unchanged4ordered = sorted(nums)5print(ordered) # [1, 1, 2, 3, 4, 5, 9]6print(nums) # [3, 1, 4, 1, 5, 9, 2]: unchanged78# sort(): in-place, returns None9nums.sort()10print(nums) # [1, 1, 2, 3, 4, 5, 9]1112# Reverse order13print(sorted(nums, reverse=True)) # [9, 5, 4, 3, 2, 1, 1]
Sorting with a key
The key parameter takes a function applied to each item before comparison. The items themselves are not changed. Only the comparison uses the key.
1words = ["banana", "fig", "apple", "date", "elderberry"]23# Sort by length4print(sorted(words, key=len))5# ['fig', 'date', 'apple', 'banana', 'elderberry']67# Sort alphabetically, case-insensitive8names = ["alice", "Bob", "charlie", "Dave"]9print(sorted(names, key=str.lower))10# ['alice', 'Bob', 'charlie', 'Dave']1112# Sort a list of dicts by a field13people = [14 {"name": "Charlie", "age": 30},15 {"name": "Alice", "age": 25},16 {"name": "Bob", "age": 35},17]18by_age = sorted(people, key=lambda p: p["age"])19print([p["name"] for p in by_age]) # ['Alice', 'Charlie', 'Bob']2021# Sort by multiple fields: age ascending, then name alphabetically22by_age_then_name = sorted(people, key=lambda p: (p["age"], p["name"]))23print([p["name"] for p in by_age_then_name])
Python's sort is stable: items that compare equal keep their original order. This matters when sorting by multiple criteria in sequence: sort by a secondary key first, then by the primary key, and ties in the primary key will preserve the secondary order.
Transforming lists
Transforming means applying the same operation to every item and collecting the results into a new list. The length never changes. Every item goes in, every item comes out changed.
In a comprehension, the expression before for is the transform. Whatever that expression evaluates to becomes the item in the new list.
1nums = [1, 2, 3, 4, 5]23# Double every number4doubled = [x * 2 for x in nums]5print(doubled) # [2, 4, 6, 8, 10]67# Square every number8squared = [x ** 2 for x in nums]9print(squared) # [1, 4, 9, 16, 25]1011# Convert to strings12as_strings = [str(x) for x in nums]13print(as_strings) # ['1', '2', '3', '4', '5']1415# Works on any type: uppercase every word16words = ["hello", "world", "python"]17upper = [w.upper() for w in words]18print(upper) # ['HELLO', 'WORLD', 'PYTHON']
Python's built-in map() does the same job. It takes a function and an iterable and applies the function to each item. Comprehensions are generally preferred because the expression is written inline, no lambda needed.
1nums = [1, 2, 3, 4, 5]23# These produce identical results4doubled_comp = [x * 2 for x in nums]5doubled_map = list(map(lambda x: x * 2, nums))67print(doubled_comp) # [2, 4, 6, 8, 10]8print(doubled_map) # [2, 4, 6, 8, 10]910# map() shines when you already have a named function11def fahrenheit(c):12 return c * 9/5 + 321314temps_c = [0, 20, 37, 100]15temps_f = list(map(fahrenheit, temps_c))16print(temps_f) # [32.0, 68.0, 98.6, 212.0]
Pick a transform below and watch every item change at once.
Filtering lists
Filtering means keeping only the items that satisfy a condition and discarding the rest. The original list is never touched. You get back a new, shorter list.
The clearest way to filter in Python is a comprehension with an if clause. You read it left to right: give me each x from nums, but only if the condition is true.
1nums = [1, 2, 3, 4, 5, 6, 7, 8]23# Keep only even numbers4evens = [x for x in nums if x % 2 == 0]5print(evens) # [2, 4, 6, 8]67# Keep numbers greater than 48big = [x for x in nums if x > 4]9print(big) # [5, 6, 7, 8]1011# Works on strings too: keep non-empty values12words = ["hello", "", "world", " ", "python"]13non_empty = [w for w in words if w.strip()]14print(non_empty) # ['hello', 'world', 'python']
Python also has a built-in filter() function that does the same thing. Comprehensions are preferred because they don't require a lambda and read more naturally.
1nums = [1, 2, 3, 4, 5]23# These produce identical results4evens_comp = [x for x in nums if x % 2 == 0]5evens_filter = list(filter(lambda x: x % 2 == 0, nums))67print(evens_comp) # [2, 4]8print(evens_filter) # [2, 4]
See it live. Pick a condition below and watch which items survive.
Real-world patterns
Deduplicate while preserving order
Converting to a set removes duplicates but loses order. This pattern preserves it.
1items = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]23# set(): fast but unordered4unique_unordered = list(set(items))56# dict.fromkeys(): preserves insertion order (Python 3.7+)7unique_ordered = list(dict.fromkeys(items))8print(unique_ordered) # [3, 1, 4, 5, 9, 2, 6]
Chunk a list into batches
1def chunks(lst, size):2 return [lst[i:i + size] for i in range(0, len(lst), size)]34data = list(range(10))5print(chunks(data, 3))6# [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
Use a list as a stack
Lists make an efficient stack with append() and pop(). Both are O(1).
1stack = []23stack.append("first")4stack.append("second")5stack.append("third")67print(stack.pop()) # "third": last in, first out8print(stack.pop()) # "second"9print(stack) # ["first"]1011# Practical: undo history12history = []13history.append(("type", "H"))14history.append(("type", "i"))15history.append(("delete",))1617last = history.pop() # undo last action
zip: iterate two lists in parallel
zip() pairs up items from two or more lists by position and lets you unpack them together in a loop. Index zero from each list, then index one, then index two. No manual indexing required.
1names = ["Alice", "Bob", "Charlie"]2scores = [92, 85, 78]34for name, score in zip(names, scores):5 print(f"{name}: {score}")
Shortest wins by default. When the lists are different lengths, zip() stops as soon as the shorter one runs out. The extra items in the longer list are silently ignored.
1letters = ["a", "b", "c", "d"]2nums = [1, 2]34for letter, num in zip(letters, nums):5 print(letter, num)6# "c" and "d" are never reached
Longest with a fill value. If you need every item from every list, use zip_longest from itertools. It pads the shorter list with a fill value you choose instead of stopping early.
1from itertools import zip_longest23letters = ["a", "b", "c", "d"]4nums = [1, 2]56for letter, num in zip_longest(letters, nums, fillvalue=0):7 print(letter, num)
Use zip() when mismatched lengths are a bug and you want to fail fast. Use zip_longest() when shorter lists are expected and a default value makes sense.
enumerate: index and value together
When you need both the position and the value while looping, enumerate() gives you both at once as a pair. It is the standard Python way to track position inside a loop without reaching for the index manually.
The common alternative, range(len(fruits)), works but forces you to index back into the list on every iteration. It is noisier, easier to get wrong, and signals to other Python developers that you are not yet thinking in Python idioms.
1fruits = ["apple", "banana", "cherry"]23# Avoid: manual index lookup on every iteration4for i in range(len(fruits)):5 print(i, fruits[i])67# Prefer: enumerate gives you both directly8for i, fruit in enumerate(fruits):9 print(i, fruit)
The start parameter lets you begin counting from any number. Useful for user-facing output where 0-based numbering looks wrong.
1fruits = ["apple", "banana", "cherry"]23for i, fruit in enumerate(fruits, start=1):4 print(f"{i}. {fruit}")
If you catch yourself writing range(len(...)) to get an index, stop and use enumerate() instead. It is almost always the right call.
Flatten and group
Flattening collapses a list of lists into a single list. The nested comprehension pattern works for one level deep. You iterate over each sublist, then over each item inside it. Outer loop first, inner loop second. The same rule as always.
1nested = [[1, 2], [3, 4], [5, 6]]2flat = [x for sublist in nested for x in sublist]3print(flat) # [1, 2, 3, 4, 5, 6]
Grouping collects consecutive equal items together. itertools.groupby() works like SQL GROUP BY but only on runs of identical adjacent values. If you want to group all matching values regardless of position, sort the list first.
1from itertools import groupby23data = ["a", "a", "b", "b", "b", "a"]4groups = [(key, list(group)) for key, group in groupby(data)]5print(groups)6# [('a', ['a', 'a']), ('b', ['b', 'b', 'b']), ('a', ['a'])]78# Sort first to group all matching values together9data_sorted = sorted(data)10all_groups = [(key, list(group)) for key, group in groupby(data_sorted)]11print(all_groups)12# [('a', ['a', 'a', 'a']), ('b', ['b', 'b', 'b'])]