Python Lists and Dictionaries
Master Python's two most-used data structures — lists and dictionaries. From indexing and slicing to accumulator patterns and frequency counters, you will write real functions that process collections of data.
2 modules · 8 lessons · free to read
What you'll learn
- ✓Create, index, slice, and modify Python lists using built-in methods and loops
- ✓Apply accumulator patterns to build, filter, and summarise lists with sum(), min(), and max()
- ✓Create and query Python dictionaries using key-value access, safe lookups with .get(), and iteration
- ✓Implement frequency-counting and tallying patterns using dictionaries as counters
01Lists
Learn how to create, access, modify, and loop over Python lists — the language's most versatile data structure. You will write functions that index, slice, sort, and accumulate values from lists.
1.Creating and Indexing Lists
A list is an ordered, mutable sequence of values — Python's most flexible container for keeping multiple items together under a single name. You create a list with square brackets, separating values by commas, and Python remembers the order you put things in.
pythonfruits = ["apple", "banana", "cherry"] numbers = [10, 20, 30, 40, 50] mixed = [1, "hello", True] empty = [] print(len(fruits)) # 3
You can also call list() to convert another iterable (like a string or range) into a list: list(range(5)) produces [0, 1, 2, 3, 4]. The built-in len() function returns the number of items in a list — useful for bounds-checking and loop control.
Indexing gives you a single element from a list. Python uses zero-based indexing: the first item is at position 0, the second at 1, and so on. Negative indices count from the end — -1 is the last item, -2 is the second-to-last. Index beyond the list's length and Python raises an IndexError.
pythonfruits = ["apple", "banana", "cherry"] print(fruits[0]) # apple print(fruits[-1]) # cherry print(fruits[1]) # banana
Slicing extracts a sub-list using list[start:stop] — it returns every item from start up to (but not including) stop. You can omit either boundary to mean "from the beginning" or "to the end": fruits[:2] gives the first two items; fruits[1:] gives everything from index 1 onward. Slices never raise IndexError — an out-of-range stop just clamps to the list length. Use slicing whenever you need a portion of a list — it is one of the patterns that appears in nearly every real Python program that processes sequences.
Constraints
- –Use a slice with `start` and `stop` indices — do not use individual index access.
- –Return a list, not a single element.
- –You may assume the input list always has at least 3 elements.
2.List Methods
Python lists come with built-in methods — functions attached to the list object that let you add, remove, sort, or copy items. The most important distinction to make from the start: some methods modify the list in place (they change the list and return None), while others return a new value without changing the original.
pythonfruits = ["banana", "apple", "cherry"] fruits.append("date") # in-place: adds to end → None returned fruits.insert(1, "avocado") # in-place: inserts at index 1 fruits.remove("apple") # in-place: removes first occurrence popped = fruits.pop() # in-place: removes & RETURNS last item print(popped) # "cherry" (NOT None) print(fruits) # ["banana", "avocado", "date"]
Notice that .pop() is the exception among mutating methods — it both modifies the list and returns the removed item. You can also call .pop(i) with an index to remove and return a specific position.
Sorting and copying require extra care. .sort() sorts the list in place and returns None — a very common mistake is to write nums = nums.sort(), which overwrites your list with None. .reverse() likewise reverses in place and returns None. When you need to work on a separate copy, use .copy() (or the slice [:]), which returns a brand-new list with the same elements.
pythonnums = [3, 1, 4, 1, 5, 9] nums.sort() # in-place: [1, 1, 3, 4, 5, 9] print(nums) # [1, 1, 3, 4, 5, 9] original = [5, 2, 8] copy = original.copy() copy.sort() print(original) # [5, 2, 8] — unchanged print(copy) # [2, 5, 8]
Use in-place methods when you want to update the existing list. Use .copy() before a mutating operation any time you need to keep the original intact — a rule of thumb that prevents many subtle bugs in Python programs.
Constraints
- –Do not modify the original `items` list — create a copy first using `.copy()`.
- –Use `.sort()` on the copy, not the built-in `sorted()` function.
- –Return the sorted copy, not `None`.
3.Looping Over Lists
The for item in list loop is Python's idiomatic way to visit every element in a list exactly once. You name the loop variable, and Python automatically advances through each position for you — no index arithmetic needed.
pythonscores = [88, 92, 75, 61] for score in scores: print(score) # prints: 88 92 75 61 (each on its own line)
When you need both the index and the value at the same time, use enumerate(). It wraps the iterable and yields (index, value) pairs. You unpack them directly in the loop header, which is cleaner and less error-prone than managing an index counter by hand.
pythonnames = ["Alice", "Bob", "Carol"] for i, name in enumerate(names): print(f"{i}: {name}") # 0: Alice # 1: Bob # 2: Carol
A list comprehension is a compact way to build a new list by transforming or filtering an existing one — all in a single expression. The basic form is [expression for item in iterable]. Python evaluates expression for every item and collects the results into a new list, saving you from writing a loop + .append() every time.
pythonnums = [1, 2, 3, 4, 5] squares = [n ** 2 for n in nums] print(squares) # [1, 4, 9, 16, 25] words = ["hello", "world"] upper = [w.upper() for w in words] print(upper) # ["HELLO", "WORLD"]
List comprehensions are best for simple transformations where the expression fits on one line. Use a regular for loop when the body involves multiple steps or side-effects. Learning to recognise when a comprehension makes code clearer — versus when it makes it harder to read — is one of the hallmarks of writing idiomatic Python.
Constraints
- –Use a list comprehension — do not use a for loop with .append().
- –Return a new list — do not modify the original `items` list.
- –The function must work for any list of numbers, including an empty list.
4.Accumulator Patterns
The accumulator pattern is a fundamental programming technique: start with an empty container (a list or a number), loop over an input sequence, and build up the result one item at a time. For lists, you initialise result = [] before the loop and call result.append(item) inside it.
pythondef evens(numbers): result = [] for n in numbers: if n % 2 == 0: result.append(n) return result print(evens([1, 2, 3, 4, 5, 6])) # [2, 4, 6]
Adding a condition inside the loop is the standard way to filter a list — you only append items that satisfy a test. This pattern is so common that Python's list comprehension syntax has a built-in shorthand for it: [x for x in items if condition]. Both the loop version and the comprehension version are valid; choose whichever is clearer for the situation.
Python also gives you three aggregate functions that work directly on any iterable of numbers: sum() adds all values, min() returns the smallest, and max() returns the largest. Using these built-ins avoids writing manual accumulator loops for the most common numeric summaries.
pythonscores = [88, 92, 75, 61, 95] print(sum(scores)) # 411 print(min(scores)) # 61 print(max(scores)) # 95 average = sum(scores) / len(scores) print(average) # 82.2
The accumulator pattern, filtering with conditions, and the aggregate built-ins form a complete toolkit for answering questions about lists: What items meet a criterion? What is the total? What are the extremes? Combining them — for example, filtering first, then calling sum() on the result — is how you solve the majority of list-processing problems in Python.
Constraints
- –Use `sum()` and `len()` to compute the average — do not hard-code any value.
- –Return a list of the numbers that are strictly greater than (not equal to) the average.
- –You may assume the input list has at least one element.
02Dictionaries
Learn how to store, retrieve, and transform data using Python's dictionary — the language's key-value powerhouse. You will write functions that query dictionaries, loop over them, and implement real-world patterns like frequency counting.
1.Key-Value Pairs
A dictionary is an unordered collection of key-value pairs — each key is a unique label that maps to a value, letting you look up any entry by name rather than by position. You create a dictionary with curly braces, separating each pair with a colon and pairs from each other with commas.
pythonstudent = {"name": "Alice", "age": 14, "grade": "A"} print(student["name"]) # Alice print(student["age"]) # 14
To update an existing key, assign a new value using the same bracket syntax. To add a brand-new key, assign to a key that does not yet exist — Python creates it automatically. Accessing a key that does not exist raises a KeyError, so it is good practice to check membership with the in operator before accessing an uncertain key.
pythonstudent["age"] = 15 # update existing key student["school"] = "Zuzu" # add new key print("phone" in student) # False — safe membership check
Dictionaries can hold any type as a value — strings, numbers, booleans, lists, or even other dictionaries. Keys must be immutable (strings, numbers, and tuples work; lists do not). You can delete a key-value pair with the del statement, and you can find out how many pairs are in a dictionary with len().
pythonscores = {"math": 88, "english": 92, "science": 75} print(len(scores)) # 3 del scores["science"] print(len(scores)) # 2 print("science" in scores) # False
Dictionaries are the right tool whenever your data has meaningful labels — a student's profile, a word's definition, a product's attributes. Reaching for a dictionary instead of a list is a sign that you are thinking about your data the way Python does.
Constraints
- –Access the values using bracket notation: `person['first']` and `person['last']`.
- –Return a single string — do not use print().
- –Do not use .get() — the keys are guaranteed to exist.
2.Dictionary Methods
Python dictionaries come with a set of built-in methods that make common operations safer and more expressive. The most important is .get(key, default): it returns the value for key if it exists, or the default value you provide instead of raising a KeyError. This is the standard way to do safe lookups.
pythonscores = {"Alice": 88, "Bob": 75} print(scores.get("Alice", 0)) # 88 print(scores.get("Carol", 0)) # 0 — key missing, default returned
.keys(), .values(), and .items() return view objects of the dictionary's keys, values, and key-value pairs respectively. They are most useful when you want to loop over them (covered in the next lesson) or convert them to a list with list().
pythond = {"x": 1, "y": 2, "z": 3} print(list(d.keys())) # ['x', 'y', 'z'] print(list(d.values())) # [1, 2, 3] print(list(d.items())) # [('x', 1), ('y', 2), ('z', 3)]
.update(other) merges all key-value pairs from other into the dictionary, overwriting any existing keys. .pop(key) removes a key and returns its value (like list .pop(), but you specify the key). .setdefault(key, default) is a powerful compound operation: if the key exists it returns its value; if not, it inserts the key with default and returns default.
pythonconfig = {"debug": True} config.update({"version": 2, "debug": False}) # merges + overwrites print(config) # {'debug': False, 'version': 2} removed = config.pop("version") print(removed) # 2 config.setdefault("timeout", 30) # key missing: inserts 30 config.setdefault("debug", True) # key exists: leaves False, returns False print(config) # {'debug': False, 'timeout': 30}
Knowing which method to reach for — .get() for safe access, .setdefault() for conditional insertion, .update() for merging — lets you write dictionary operations as clear, single-line expressions rather than multi-line if-else blocks.
Constraints
- –Use `.get()` with a default value of `-1` — do not use an if/else or the `in` operator.
- –Return the value directly — do not use print().
- –The function must return the integer -1 (not None or False) when the key is missing.
3.Looping Over Dictionaries
When you write for k in d, Python iterates over the dictionary's keys in insertion order (guaranteed since Python 3.7). This is the simplest way to visit every key, and from each key you can access its value with d[k].
pythonprices = {"apple": 0.5, "banana": 0.3, "cherry": 1.2} for item in prices: print(item, prices[item]) # apple 0.5 # banana 0.3 # cherry 1.2
The more idiomatic approach when you need both the key and the value is for k, v in d.items(). It unpacks each (key, value) tuple directly in the loop header — cleaner and more readable than looking up d[k] inside the loop.
pythonfor item, price in prices.items(): print(f"{item}: ${price:.2f}") # apple: $0.50 # banana: $0.30 # cherry: $1.20
You can also build a dictionary by looping over a list — a common pattern for transforming data. Start with result = {}, then inside a loop assign to new keys: result[key] = value. This is the dictionary equivalent of the list accumulator pattern.
pythonwords = ["cat", "dog", "fish"] lengths = {} for word in words: lengths[word] = len(word) print(lengths) # {'cat': 3, 'dog': 3, 'fish': 4}
Use for k in d when you only need keys, for k, v in d.items() when you need both, and the build-dict pattern when you need to create a mapping from a list. Combining these three forms covers the vast majority of dictionary-processing tasks you will encounter.
Constraints
- –Use `for k, v in d.items()` to iterate over the dictionary.
- –Return a new dictionary — do not modify the input dictionary `d`.
- –You may assume all values in `d` are unique and hashable (so they can be used as keys).
4.Dictionaries as Counters
The frequency-counting pattern is one of the most useful patterns in Python: use a dictionary to count how many times each item appears in a sequence. The standard idiom uses .get() with a default of 0 — if the key already exists, increment its count; if it does not, start from 0 and add 1.
pythondef count_chars(s): counts = {} for ch in s: counts[ch] = counts.get(ch, 0) + 1 return counts print(count_chars("hello")) # {'h': 1, 'e': 1, 'l': 2, 'o': 1}
The single line counts[ch] = counts.get(ch, 0) + 1 does all the work: it reads the current count (defaulting to 0 if missing) and writes back the incremented value. This works for any hashable item — characters, words, numbers, or tuples.
For more complex tallying you can build dicts of lists — also called grouped or nested dictionaries. Instead of counting occurrences, you append each item to a list stored under a key. The pattern mirrors the counter, but uses .setdefault() (or .get() with [] as default) to ensure the list exists before appending.
pythondef group_by_length(words): groups = {} for word in words: length = len(word) groups.setdefault(length, []).append(word) return groups print(group_by_length(["cat", "dog", "fish", "ox"])) # {3: ['cat', 'dog'], 4: ['fish'], 2: ['ox']}
The frequency-counter pattern appears in word analysis, vote tallying, histogram generation, and any problem that asks "how many times does X appear?" Mastering it — and its grouped variant — gives you a powerful tool for a wide class of real-world data problems.
Constraints
- –Use `.get(word, 0)` to safely read the current count before incrementing.
- –Return a dictionary — do not print anything.
- –Each key in the returned dictionary must be a word from the list; each value must be an integer count.
Frequently Asked Questions
- What does `fruits[-1]` return when `fruits = ['apple', 'banana', 'cherry']`?
- cherry. Negative indices count from the end of the list. Index -1 is the last element, which is 'cherry'.
- Which statement about `.sort()` is correct?
- It sorts the list in place and returns None.. .sort() is an in-place method — it modifies the list directly and returns None, not the sorted list. Assigning the return value of .sort() is a common mistake.
- What is the output of `[n ** 2 for n in [1, 2, 3]]`?
- [1, 4, 9]. A list comprehension evaluates the expression (n ** 2) for each item in the iterable, collecting the results into a new list: [1, 4, 9].
- What does `enumerate(['a', 'b', 'c'])` produce when used in a for loop?
- Pairs of (index, value): (0,'a'), (1,'b'), (2,'c'). enumerate() wraps an iterable and yields (index, value) pairs, so you can unpack both the position and the item in the loop header.
- You want to build a list of items from `data` that are greater than 10. Which pattern is correct?
- result = []; for x in data: if x > 10: result.append(x). The accumulator pattern starts with an empty list and uses .append() inside a loop to add items that pass the condition. Option b uses assignment instead of .append(), which would overwrite result with a single integer.
- How do I write a Python function called `second_and_third` that takes a list `items` and returns a new list containing only the second and third elements (indices 1 and 2), using list slicing.?
- A **list** is an ordered, mutable sequence of values — Python's most flexible container for keeping multiple items together under a single name. You create a list with square brackets, separating values by commas, and Python remembers the order you put things in.
- How do I write a Python function called `sorted_copy` that takes a list `items` and returns a new sorted list, leaving the original list unchanged — use `.copy()` and `.sort()`.?
- Python lists come with built-in **methods** — functions attached to the list object that let you add, remove, sort, or copy items. The most important distinction to make from the start: some methods **modify the list in place** (they change the list and return `None`), while others return a new value without changing the original.
- How do I write a Python function called `doubled` that takes a list of numbers `items` and returns a new list where every element has been multiplied by 2, using a list comprehension.?
- The `for item in list` loop is Python's idiomatic way to visit every element in a list exactly once. You name the loop variable, and Python automatically advances through each position for you — no index arithmetic needed.
- How do I write a Python function called `above_average` that takes a list of numbers, computes the average using `sum()` and `len()`, and returns a new list containing only the numbers that are strictly greater than the average.?
- The **accumulator pattern** is a fundamental programming technique: start with an empty container (a list or a number), loop over an input sequence, and build up the result one item at a time. For lists, you initialise `result = []` before the loop and call `result.append(item)` inside it.
- What happens when you access `d['missing_key']` and the key does not exist?
- Python raises a KeyError.. Bracket access raises a KeyError when the key is missing. Use .get() with a default value if you want a safe lookup that doesn't raise an error.
- What does `scores.get('Alice', 0)` return when 'Alice' is NOT in `scores`?
- 0. .get(key, default) returns the default value (0 in this case) when the key is missing — it never raises a KeyError.
- Which loop form gives you BOTH the key and value from a dictionary?
- for k, v in d.items():. d.items() yields (key, value) tuples, which you unpack directly in the loop header. The other forms give only keys or only values.
- What does `counts[word] = counts.get(word, 0) + 1` do when `word` is NOT yet in `counts`?
- Sets counts[word] to 1.. .get(word, 0) returns 0 when the key is missing, then 0 + 1 = 1 is assigned to counts[word]. This is the standard one-line frequency-counter pattern.
- You have `d = {'x': 10}`. What does `d.setdefault('x', 99)` return?
- 10. .setdefault(key, default) returns the existing value when the key is already present — it only inserts the default when the key is missing. Here 'x' already maps to 10, so 10 is returned.
- How do I write a Python function called `get_full_name` that takes a dictionary `person` with keys `'first'` and `'last'` and returns the full name as a single string with a space between them, using dictionary key access.?
- A **dictionary** is an unordered collection of **key-value pairs** — each key is a unique label that maps to a value, letting you look up any entry by name rather than by position. You create a dictionary with curly braces, separating each pair with a colon and pairs from each other with commas.
- How do I write a Python function called `lookup_score` that takes a dictionary `scores` and a string `name`, and returns the score for that name using `.get()` — returning `-1` if the name is not in the dictionary.?
- Python dictionaries come with a set of built-in methods that make common operations safer and more expressive. The most important is `.get(key, default)`: it returns the value for `key` if it exists, or the `default` value you provide instead of raising a `KeyError`. This is the standard way to do safe lookups.
- How do I write a Python function called `invert_dict` that takes a dictionary `d` and returns a new dictionary with keys and values swapped — each value in the original becomes a key in the result, mapping back to its original key.?
- When you write `for k in d`, Python iterates over the dictionary's **keys** in insertion order (guaranteed since Python 3.7). This is the simplest way to visit every key, and from each key you can access its value with `d[k]`.
- How do I write a Python function called `word_count` that takes a list of strings `words` and returns a dictionary mapping each unique word to the number of times it appears in the list, using the `.get()` counter pattern.?
- The **frequency-counting pattern** is one of the most useful patterns in Python: use a dictionary to count how many times each item appears in a sequence. The standard idiom uses `.get()` with a default of `0` — if the key already exists, increment its count; if it does not, start from 0 and add 1.
Ready to write code?
Theory is just the start. Write real code, run tests, build the habit.
Open the playground →