Python · lesson 9 of 34
Beginner
16- 1 Introduction to Python
- 2 Setting Up Python
- 3 Python Variables and Type System
- 4 Python Data Types
- 5 Python Operators
- 6 Python Strings
- 7 Control Flow in Python
- 8 Python Functions and Parameters
- 9 Python Data Structures
- 10 Python Collections
- 11 Python Modules and Packages
- 12 Object-Oriented Programming in Python
- 13 Encapsulation and Name Mangling in Python
- 14 Polymorphism and Duck Typing in Python
- 15 Abstraction with Abstract Base Classes in Python
- 21 Building a Python CLI Tool
Intermediate
12- 16 Multiple Inheritance and MRO in Python
- 17 Composition vs Inheritance in Python
- 18 Python Dataclasses
- 19 Error Handling in Python
- 20 File I/O, pathlib, and JSON in Python
- 22 Python Decorators and Closures
- 23 Python Generators
- 24 Python Context Managers
- 25 Python Concurrency
- 27 Testing Python Code
- 28 Python Security
- 34 Python Interview Prep
Advanced
6Python Data Structures
Use Python lists, tuples, dicts, sets and comprehensions through small runnable programs, plus a real timing of list vs set membership tests.
By SkillByExample · Updated
Output below is from Python 3.13.
Lists — Ordered, Mutable Sequences
Lists hold an ordered collection of items of any type and can be changed after creation. They’re the go-to structure when you need to maintain order, allow duplicates, and modify contents over time — appending results, sorting records, or building up a collection incrementally.
# lists.py
scores = [95, 82, 78, 91, 88]
print(scores[0])
print(scores[-1])
print(scores[1:3])
scores.append(100)
scores.insert(0, 70)
scores.remove(78)
popped = scores.pop()
scores.sort(reverse=True)
sorted_scores = sorted(scores)
print(len(scores))
print(scores.count(82))
print(scores.index(91))
$ python lists.py
95
88
[82, 78]
5
1
1
By the time len runs, the list is [95, 91, 88, 82, 70]: append(100) was undone by pop(), insert(0, 70) added a value that sorting moved to the end, and remove(78) deleted it. That’s why index(91) is 1 — positions refer to the list after the in-place sort(reverse=True), while sorted_scores is a separate ascending copy.
List Comprehensions
List comprehensions create a new list by applying an expression to each element of an iterable, with an optional filter. They’re preferred over for loops with .append() because they’re more concise and compiled to faster bytecode.
# comprehensions.py
numbers = range(1, 11)
squares = [n ** 2 for n in numbers]
print(squares)
even_squares = [n ** 2 for n in numbers if n % 2 == 0]
print(even_squares)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [cell for row in matrix for cell in row]
print(flat)
$ python comprehensions.py
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
[4, 16, 36, 64, 100]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
Tuples — Ordered, Immutable Sequences
Tuples behave like lists but cannot be modified after creation. Their immutability is their value: it signals to the reader that the data is fixed, it allows tuples to be used as dictionary keys, and it makes them slightly faster to create and access than lists.
# tuples.py
point = (10.5, 20.3)
x, y = point
from collections import namedtuple
Colour = namedtuple("Colour", ["red", "green", "blue"])
white = Colour(red=255, green=255, blue=255)
print(white.red)
print(white)
grid = {(0, 0): "start", (3, 4): "end"}
print(grid[(0, 0)])
$ python tuples.py
255
Colour(red=255, green=255, blue=255)
start
Dictionaries — Key-Value Maps
Dictionaries store key-value pairs and provide O(1) average-time lookup. They’re the right choice when you need to associate pieces of data by a meaningful key rather than a positional index — user records, configuration objects, counters, and caches are all natural fits. Since Python 3.7, insertion order is preserved.
# dicts.py
user = {
"name": "Alice",
"age": 30,
"email": "alice@example.com",
}
print(user["name"])
print(user.get("phone", "N/A"))
user["age"] = 31
user["role"] = "admin"
del user["email"]
for key, value in user.items():
print(f"{key}: {value}")
word = "hello"
char_count = {char: word.count(char) for char in word}
print(char_count)
$ python dicts.py
Alice
N/A
name: Alice
age: 31
role: admin
{'h': 1, 'e': 1, 'l': 2, 'o': 1}
Reassigning age kept it in its original position; only the new role key went to the end, and deleting email closed the gap. The comprehension iterates over word itself, so keys appear in first-seen order and the repeated l simply overwrites its own entry.
Merging Dictionaries (Python 3.9+)
The | operator merges two dicts into a new one. Keys from the right-hand dict override keys from the left, which makes it natural for applying user overrides on top of defaults.
# merge_dicts.py
defaults = {"theme": "dark", "lang": "en"}
overrides = {"lang": "fr", "timezone": "UTC"}
merged = defaults | overrides
print(merged)
$ python merge_dicts.py
{'theme': 'dark', 'lang': 'fr', 'timezone': 'UTC'}
Sets — Unordered, Unique Collections
Sets automatically deduplicate their contents and provide O(1) membership testing — the same speed as a dict lookup, much faster than scanning a list. They’re the right choice when you need uniqueness guarantees or want to compute relationships between groups of items (intersection, union, difference).
# sets.py
tags = {"python", "web", "api", "python"}
print(len(tags), sorted(tags))
print("python" in tags)
backend = {"python", "django", "postgres"}
frontend = {"javascript", "react", "python"}
shared = backend & frontend
all_tech = backend | frontend
only_backend = backend - frontend
either_not_both = backend ^ frontend
print(sorted(shared))
print(sorted(all_tech))
print(sorted(only_backend))
print(sorted(either_not_both))
tags.add("backend")
tags.discard("api")
print(sorted(tags))
$ python sets.py
3 ['api', 'python', 'web']
True
['python']
['django', 'javascript', 'postgres', 'python', 'react']
['django', 'postgres']
['django', 'javascript', 'postgres', 'react']
['backend', 'python', 'web']
The duplicate "python" collapsed, leaving three tags. Every set is passed through sorted() before printing for a reason: string hashes are randomised per process, so printing a set of strings directly gives a different order on each run. &, |, - and ^ are intersection, union, difference and symmetric difference.
Choosing the Right Structure
Picking the wrong data structure usually shows up as slow code or awkward access patterns. This table summarizes the key trade-offs:
| Use case | Best type |
|---|---|
| Ordered, changeable collection | list |
| Fixed record (coordinates, config row) | tuple |
| Fast key-value lookup | dict |
| Membership tests, deduplication | set |
| Immutable set (used as a dict key) | frozenset |
The most common mistake is using a list for membership testing when the collection is large. Searching a list is O(n); a set lookup is O(1). If you’re writing if item in my_list inside a loop, convert my_list to a set first.
# membership_timing.py
import time
big_list = list(range(100_000))
big_set = set(big_list)
item = 99_999
start = time.perf_counter()
for _ in range(1000):
item in big_list
list_time = time.perf_counter() - start
start = time.perf_counter()
for _ in range(1000):
item in big_set
set_time = time.perf_counter() - start
print(f"List lookup: {list_time:.4f}s")
print(f"Set lookup: {set_time:.4f}s")
print(f"Speedup: {list_time / set_time:.1f}x")
$ python membership_timing.py
List lookup: 0.7169s
Set lookup: 0.0001s
Speedup: 9489.2x
Timings vary from run to run and machine to machine, but the set lookup is thousands of times faster here: in on a list compares against every element until it finds 99_999 at the very end, while the set hashes the item and jumps straight to its slot. The gap widens as the collection grows.
Practice
1. List mutation chain: Given a list of scores, write code that removes the lowest and highest scores, then returns the average of the remaining middle values. Predict the result for [100, 85, 92, 78, 90, 88].
Solution
scores = [100, 85, 92, 78, 90, 88]
scores.remove(max(scores))
scores.remove(min(scores))
print(sum(scores) / len(scores))
$ python middle_average.py
88.75
After removing the max (100) and min (78), the remaining scores are [85, 92, 90, 88]. Their sum is 355, divided by 4 gives 88.75.
2. Dictionary merge with override: Create two dictionaries — one with defaults and one with user settings — then merge them so user settings override defaults. Print the merged result.
Solution
defaults = {"host": "localhost", "port": 5432, "debug": False}
user_settings = {"port": 8080, "debug": True}
merged = defaults | user_settings
print(merged)
$ python config_merge.py
{'host': 'localhost', 'port': 8080, 'debug': True}
The | operator merges left-to-right with right-side keys winning on conflicts. "port" changed to 8080 and "debug" changed to True, while "host" stayed as "localhost".
3. Set deduplication in log analysis: Given a list of log levels from a log file, use a set to find all unique levels and count how many times “ERROR” appears.
Solution
log_levels = ["INFO", "WARN", "ERROR", "INFO", "ERROR", "DEBUG", "ERROR", "WARN"]
unique_levels = set(log_levels)
error_count = log_levels.count("ERROR")
print(sorted(unique_levels))
print(error_count)
$ python log_analysis.py
['DEBUG', 'ERROR', 'INFO', 'WARN']
3
The set automatically removes duplicates, giving you just ['DEBUG', 'ERROR', 'INFO', 'WARN']. The .count() method counts how many times “ERROR” appears — 3 in this case.