Skip to main content

Collections as Types

In Chapter 47, every variable held a single value: one number, one string, one boolean. Real applications need groups of values. A note has multiple tags. A quiz has multiple scores. A project has multiple contributors. You need types that hold collections of values, not just individual ones.

James reaches the point where SmartNotes needs tags. He types:

tags: str = "python,ai,beginner"

Emma looks at his screen. "How do you add a new tag?"

"String concatenation. tags = tags + ',web'."

"How do you check if a note has the 'ai' tag?"

James hesitates. Searching for "ai" in the string would also match a tag like "rail" because "ai" appears inside it.

Emma replaces his line with: tags: list[str] = ["python", "ai", "beginner"]. "Now each tag is a separate element. 'ai' in tags checks the whole tag, not substrings. And pyright knows every element is a str."

James nods. Then he pushes back: "But for scores, I need names and numbers together. A list cannot do that."

"You are right. That is what dicts are for." She writes: scores: dict[str, int] = {"quiz1": 85, "quiz2": 92}.

James pauses. "It is like a lookup table. You give it a name, it gives you the number."

Emma smiles. "That is actually a better explanation than mine. I was going to say 'key-value mapping,' but 'lookup table' is clearer."


Lists: Ordered, Typed Groups

A list holds multiple values in order. The type annotation specifies what type the elements are:

tags: list[str] = ["python", "ai", "beginner"]
scores: list[int] = [85, 92, 78, 95]
prices: list[float] = [9.99, 14.50, 3.25]

Output (from print statements):

['python', 'ai', 'beginner']
[85, 92, 78, 95]
[9.99, 14.5, 3.25]

list[str] means "a list where every element is a string." Pyright enforces this. If you try to add an integer to a list[str], pyright flags it.

Accessing List Elements

Lists use the same indexing as strings (zero-based):

tags: list[str] = ["python", "ai", "beginner"]
tags[0]
tags[-1]
len(tags)
"ai" in tags

Output:

python
beginner
3
True
OperationWhat It DoesReturns
tags[0]First elementstr
tags[-1]Last elementstr
len(tags)Number of elementsint
"ai" in tagsChecks membershipbool
If you're new to programming

A list is like a numbered shelf. Each slot holds one item, and you access items by their slot number (starting from 0). The type annotation list[str] tells Python (and pyright) that every slot holds a string. If you try to put a number on the shelf, pyright warns you.


Dicts: Key-Value Lookup Tables

A dict (dictionary) maps keys to values. Each key has exactly one value:

scores: dict[str, int] = {"quiz1": 85, "quiz2": 92}
metadata: dict[str, str] = {"author": "James", "topic": "Python"}

Output (from print statements):

{'quiz1': 85, 'quiz2': 92}
{'author': 'James', 'topic': 'Python'}

dict[str, int] means "a dict where keys are strings and values are integers." The first type in the brackets is the key type; the second is the value type.

Accessing Dict Values

Use the key in square brackets to get the value:

scores: dict[str, int] = {"quiz1": 85, "quiz2": 92}
scores["quiz1"]
len(scores)
"quiz1" in scores

Output:

85
2
True

If you access a key that does not exist, Python raises a KeyError:

scores: dict[str, int] = {"quiz1": 85, "quiz2": 92}
scores["quiz3"] # KeyError: 'quiz3'

Output:

KeyError: 'quiz3'

The safe alternative is .get(), which returns a default value instead of raising an error:

scores: dict[str, int] = {"quiz1": 85, "quiz2": 92}
scores.get("quiz3", 0)

Output:

0

.get("quiz3", 0) means "get the value for 'quiz3', or return 0 if the key does not exist." You will see this pattern frequently in AI-generated code.


Tuples: Fixed, Immutable Groups

A tuple holds a fixed number of values that cannot change after creation:

point: tuple[float, float] = (3.14, 2.71)
name_age: tuple[str, int] = ("James", 30)

Output (from print statements):

(3.14, 2.71)
('James', 30)

Unlike lists, tuple type annotations specify the type of each position: tuple[float, float] means "exactly two floats." tuple[str, int] means "a string in the first position, an integer in the second."

Tuples use the same indexing as lists:

point: tuple[float, float] = (3.14, 2.71)
point[0]
point[1]

Output:

3.14
2.71

But you cannot change a tuple after creation:

point: tuple[float, float] = (3.14, 2.71)
# point[0] = 1.0 # TypeError: 'tuple' object does not support item assignment

This is the same immutability concept from strings in Lesson 3. Strings and tuples cannot be changed; lists and dicts can.


Sets: Unique Elements

A set holds unique values with no duplicates and no guaranteed order:

unique_tags: set[str] = {"python", "ai", "python", "beginner"}

Output:

{'python', 'ai', 'beginner'}

The duplicate "python" was automatically removed. Sets are useful when you care about uniqueness: "Which distinct tags exist?" rather than "What is the third tag?"

unique_tags: set[str] = {"python", "ai", "beginner"}
"ai" in unique_tags
len(unique_tags)

Output:

True
3

Sets do not support indexing (unique_tags[0] raises a TypeError) because elements have no fixed position. Use sets when order does not matter and uniqueness does.


Mutability Summary

TypeMutable?Can Add/Remove Elements?Can Change Existing?
listYesYesYes
dictYesYesYes
tupleNoNoNo
setYesYesN/A (no positions)
strNoNoNo

Lists, dicts, and sets can change after creation. Tuples and strings cannot. This distinction matters when you write function signatures: a tuple[str, int] parameter guarantees the data will not be modified inside the function.


PRIMM-AI+ Practice: Predicting Collection Access

Predict [AI-FREE]

Look at these collection access expressions. Without running anything, predict the exact result of each one. Write your predictions and a confidence score from 1 to 5 before checking.

tags: list[str] = ["python", "ai", "web"]
scores: dict[str, int] = {"quiz1": 85, "quiz2": 92}
point: tuple[float, float] = (3.14, 2.71)

result1: str = tags[1]
result2: int = scores["quiz2"]
result3: float = point[0]
result4: bool = "ruby" in tags
Check your predictions

result1: "ai". Index 1 is the second element (zero-based). The list is ["python", "ai", "web"].

result2: 92. The key "quiz2" maps to the value 92.

result3: 3.14. Tuple indexing works like list indexing: position 0 is the first element.

result4: False. The tag "ruby" is not in the list ["python", "ai", "web"].

If you got all four correct, your collection access intuition is solid. The most common mistake is using the wrong index (forgetting zero-based) or confusing dict key lookup with list indexing.

Run

Create a file called collection_practice.py with the expressions above. Add print() calls for each one. Run it with uv run python collection_practice.py. Compare the output to your predictions.

Investigate

For each collection access, write one sentence explaining how Python finds the value. Lists and tuples use numeric positions; dicts use key names. What happens if you try scores[0]? (Hint: 0 is an int, not a str key.)

Modify

Add a new element to each mutable collection:

  • Add "data" to tags using tags.append("data")
  • Add "quiz3" with score 88 to scores using scores["quiz3"] = 88

Predict len(tags) and len(scores) after the additions. Run to verify.

Make [Mastery Gate]

Write type annotations for SmartNotes data from scratch:

  1. tags: list[str] with 3 tags
  2. metadata: dict[str, str] with author and topic
  3. scores: dict[str, int] with 2 quiz scores
  4. Access one element from each collection and print it

Run uv run pyright on your file. Zero errors means you pass.


Try With AI

Opening Claude Code

If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.

Prompt 1: Test Your Type Annotation Understanding

I'm learning Python collection types. Here is my understanding:
"list[str] means a list of strings, dict[str, int] means keys
are strings and values are integers, and tuples cannot be
changed after creation." Is this accurate? What am I missing?

Read AI's response. Did it mention that tuple annotations specify the type of each position (not just "a tuple of strings")? Did it explain set uniqueness? Compare its answer to what you learned in this lesson.

What you're learning: You are verifying that your mental model covers all four collection types and their key differences. Gaps in understanding here lead to bugs in function signatures later.

Prompt 2: Generate Collection Declarations

Write Python declarations for a simple student grade tracker.
I need: a list of student names, a dict mapping names to
grades (integers), a tuple representing a grade range
(minimum and maximum as floats), and a set of passing
grades. Use full type annotations on every variable.

Read AI's output. Check: did it use list[str], dict[str, int], tuple[float, float], and set[int]? Are the annotations correct for each collection? If anything looks wrong, tell AI what to fix.

What you're learning: You are reviewing AI-generated type annotations against the collection rules from this lesson. Catching type annotation errors early prevents subtle bugs when functions use these collections.


Key Takeaways

  1. list[str] means "a list where every element is a string." The type inside the brackets tells pyright what the elements should be. Use lists for ordered groups.

  2. dict[str, int] means "keys are strings, values are integers." Access values by key with scores["quiz1"] or safely with scores.get("quiz3", 0).

  3. Tuples are immutable, lists are mutable. tuple[str, int] specifies the type of each position. Once created, tuples cannot change. Lists and dicts can.

  4. Sets enforce uniqueness. set[str] automatically removes duplicates. Use sets when you care about "which distinct values exist" rather than order.


Looking Ahead

You can now annotate all four collection types. In Lesson 5, you combine them: list[dict[str, int]] is a list of score dicts, and dict[str, list[str]] maps categories to tag lists. Nested types let you specify complex data structures, and you will write the SmartNotes format_tag_list() function as your chapter TDG Challenge.