Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Activity 1.2

This page is an ipython notebook, meaning you can download it and run the executable cells. It directly follows Activity 1.2. Reference answers appear after the questions, challenges, predictions, and reflections. They show one reasonable solution or interpretation, but other correct approaches may exist.

Python Objects, Data Structures, Mutability, Typing & Comprehensions

Tools: Python, VS Code and/or Jupyter

Learning objectives

By the end of this tutorial, you should be able to:

  • inspect Python objects with type() and id();

  • distinguish names from objects;

  • explain mutable vs. immutable objects;

  • recognize shared references;

  • use lists, tuples, dictionaries, and sets;

  • explain dynamic typing;

  • use basic type annotations;

  • write list and dictionary comprehensions;

  • connect data-structure choices to statistical computing.


Part 0 — Set up

To begin this activity, open your repository for the course on GitHub, and open its Codespace.

Inside your repository create:

notebooks/week02_data_structures.ipynb

Part 1 — Everything is an object

Run:

Then:

Questions

  1. Did the type change?

  2. Did the identity change?

  3. What happened to the integer object?

  4. Does the name x have a permanent type?

Write your answers in a Markdown cell.

Reference answer

  1. Did the type change? Yes. x first refers to an integer (int) and then to a string (str).

  2. Did the identity change? Yes. Rebinding x makes it refer to a different object, so its id() changes.

  3. What happened to the integer object? The name x stopped referring to it. If no other references to that integer object remain, Python is free to reclaim it. (For small integers, Python may internally reuse objects, so the exact id() behavior should not be treated as a memory-management experiment.)

  4. Does x have a permanent type? No. The object has a type; the name x can be rebound to objects of different types.

Part 2 — Names and objects

Run:

Then:

Prediction

Now predict the value of y.

Draw:

x ─────┐
       ├────► ?
y ─────┘

Replace ? with the object.

Verify by running:

Reference answer

y is:

[1, 2, 3, 4]

x and y refer to the same list object. Therefore mutating the list through x is visible through y as well.

Part 3 — Mutation vs. rebinding

Compare:

with:

For each, inspect:

Question

What is the difference between x.append(4) and x = [1, 2, 3, 4]?

Reference answer

x.append(4) mutates the existing list in place. If y = x, both names still refer to that same list, so both see the appended value and x is y remains True.

By contrast,

x = [1, 2, 3, 4]

rebinds the name x to a new list object. y still refers to the original [1, 2, 3], so x is y becomes False.

Part 4 — Mutable vs. immutable

Create:

Create a table:

ObjectTypeMutable?
a
b
c
d
e
f
g

Test your answers with operations such as:

Try:

Explain the result.

Reference answer

ObjectTypeMutable?
aintNo
bfloatNo
cstrNo
dtupleNo
elistYes
fdictYes
gsetYes

These operations work because lists and dictionaries are mutable:

e.append(4)
f["b"] = 2

But:

d[0] = 99

raises a TypeError because tuples are immutable: an existing tuple cannot have one of its elements replaced in place.

Part 5 — A function that changes its input

Run:

Then create a non-mutating version:

Reflection

Which style would you prefer for a statistical function, and why?

Reference answer

For most statistical/data-science functions, I would usually prefer the non-mutating version unless in-place modification is intentional and clearly documented.

Returning a new object makes the function easier to reason about because the caller’s original data remain unchanged. This reduces unexpected side effects, makes debugging/testing easier, and is especially helpful when the same data object is used elsewhere in an analysis.

Mutation can still be appropriate when memory/performance matters, but it should be explicit.

Part 6 — Shallow copying

Run:

Then:

Explain why the inner list changed in both objects.

Reference answer

x.copy() makes a shallow copy. The outer lists are different objects, so:

x is y

is False.

However, the nested lists are still shared. Therefore:

x[0] is y[0]

is True.

When 99 is appended to x[0], the shared inner list changes, so the change appears inside both x and y.

Part 7 — Lists

Create:

Practice:

Challenge

Without NumPy, calculate:

  • number of observations;

  • minimum;

  • maximum;

  • mean.

Reference solution

One solution is:

n = len(scores)
minimum = min(scores)
maximum = max(scores)
mean_score = sum(scores) / len(scores)

n, minimum, maximum, mean_score

With the list as it exists after the earlier append, sort, and reverse operations, the values themselves are unchanged except that 90 was added, so the number of observations is 9, the minimum is 68, the maximum is 95, and the mean is approximately 82.89.

Part 8 — Tuples

Create:

Access:

Try:

Explain why a tuple could make conceptual sense for a coordinate pair.

Reference answer

A latitude/longitude coordinate is naturally a fixed pair of values. A tuple communicates that the pair belongs together and is not intended to be modified element-by-element.

For example:

flagstaff = (35.1983, -111.6513)

Using a tuple does not make the geographic value inherently safer or more correct, but its immutability expresses the intended structure clearly.

Part 9 — Dictionaries

Create:

Practice:

Why might .get() be preferable when a key may not exist?

Reference answer

Dictionary indexing such as:

student["email"]

raises a KeyError if the key does not exist.

By contrast:

student.get("email")

returns None by default, or a value you specify:

student.get("email", "not provided")

This is useful when a key is optional and its absence is expected rather than exceptional.

Part 10 — Sets

Create:

Test:

Explain why a set can be useful for unique categories and membership testing.

Reference answer

A set automatically keeps unique values, so converting the list of state names to a set removes duplicates.

Sets are also designed for efficient membership testing:

"Arizona" in unique_states

This makes them useful for collections such as allowed categories, observed labels, or unique groups when ordering and duplicates are not important.

Part 11 — Choosing data structures

Choose a data structure for each:

A. A sequence of ages where order matters.
B. A fixed latitude/longitude coordinate.
C. Subject ID → treatment group.
D. Unique treatment groups.
E. A collection that will be repeatedly modified.

Explain each answer.

Reference answer

A. Sequence of ages where order matters → list
Lists preserve order and can be modified.

B. Fixed latitude/longitude coordinate → tuple
A tuple represents a fixed ordered pair and communicates that it should not be changed in place.

C. Subject ID → treatment group → dict
A dictionary naturally maps a unique key (subject ID) to a value (treatment group).

D. Unique treatment groups → set
A set stores unique values and supports fast membership checks.

E. Collection repeatedly modified → usually list
Lists are mutable and support append/remove/update-style operations. The best choice could differ if the collection has key-value or uniqueness requirements.

Part 12 — Dynamic typing

Run:

Then:

Explain why Python can be dynamically typed while still having types.

Reference answer

Dynamically typed does not mean “untyped.” Every Python object still has a type.

The dynamic part is that a name is not permanently declared to have one type. For example, x can first refer to an int, then a str, then a list.

The expression:

"10" + 5

fails because Python still respects object types: it does not automatically combine a string and an integer using +.

Part 13 — Type annotations

Write:

Run:

Then:

Explain what the annotations communicate and why annotations do not make Python statically typed.

Reference answer

In:

def mean(values: list[float]) -> float:

list[float] communicates that values is intended to be a list of floating-point values, and -> float communicates that the function is intended to return a float.

These are annotations, not automatic runtime restrictions. Likewise:

def add(a: int, b: int) -> int:
    return a + b

can still be called with strings, and Python will concatenate them because the annotations are not enforced by the interpreter.

Annotations improve documentation, editor support, and static type checking, but they do not make Python statically typed.

Part 14 — List comprehensions

Traditional loop:

Rewrite:

Then:

Write the equivalent traditional loop and compare readability.

Reference solution

The traditional-loop equivalent is:

even_squares = []

for x in range(21):
    if x % 2 == 0:
        even_squares.append(x ** 2)

For this simple transformation/filter, the comprehension is arguably clearer because the operation fits naturally in one readable expression:

even_squares = [x ** 2 for x in range(21) if x % 2 == 0]

If the logic required several branches or intermediate calculations, the explicit loop would usually be easier to read.

Part 15 — Statistical example

Create:

Verify:

Then calculate the population variance from squared_deviations.

Reference solution

The population variance is the mean of the squared deviations:

population_variance = sum(squared_deviations) / len(squared_deviations)
population_variance

For these values, it is approximately:

4.1556

Because this asks for the population variance, divide by n, not n - 1.

Part 16 — Dictionary comprehensions

Run:

Create a dictionary mapping 1–10 to whether each number is even:

Part 17 — Structured observations

Create:

Extract scores:

Filter scores ≥ 90:

Create ID → score:

Challenge

Create a dictionary containing only observations with scores ≥ 85.

Reference solution

high_score_by_id = {
    obs["id"]: obs["score"]
    for obs in observations
    if obs["score"] >= 85
}

Result:

{2: 91, 4: 88, 5: 94}

Note: the earlier high_scores example says “scores ≥ 85” but its code uses >= 90. For the challenge here, use the stated threshold of 85.

Part 18 — When not to use a comprehension

Consider:

Consider whether compressing this into one comprehension would actually improve the code.

Principle

Use comprehensions when they make simple transformations clearer.

Do not use them merely to minimize lines.

Reference answer

Compressing this entire block into one comprehension would probably make it harder to read, not better.

There are two conditions plus a nontrivial transformation:

if person["age"] >= 18:
    if person["status"] == "active":
        transformed = complicated_function(person)

A comprehension can technically express this, but the explicit loop makes the control flow and intermediate transformation easier to inspect and debug. The goal is readable code, not the fewest lines.

Part 19 — Mini data-science challenge

Create:

Using comprehensions, calculate:

Then create:

Add:

Reference solution

One possible solution is:

ids = [p["id"] for p in participants]

scores = [p["score"] for p in participants]

high_scores = [
    p["score"]
    for p in participants
    if p["score"] >= 90
]

older_ids = [
    p["id"]
    for p in participants
    if p["age"] >= 35
]

groups = {p["group"] for p in participants}

score_by_id = {
    p["id"]: p["score"]
    for p in participants
}

A possible summary is:

summary = {
    "n": len(participants),
    "groups": {p["group"] for p in participants},
    "mean_score": sum(p["score"] for p in participants) / len(participants),
    "max_score": max(p["score"] for p in participants),
    "high_performers": [
        p["id"]
        for p in participants
        if p["score"] >= 90
    ]
}

This gives a mean score of approximately 85.17, a maximum score of 94, and high performers [102, 105].

A basic annotation is:

summary: dict = {
    # same contents as above
}

A more specific annotation could be used later after introducing richer typing tools.

Part 20 — Compare loops and comprehensions

Choose one previous problem and implement it both ways.

Answer:

  1. Which is shorter?

  2. Which is clearer?

  3. Which would you prefer in production code?

  4. Would your answer change if the transformation became more complicated?

Reference answer

For a simple example such as squaring even numbers:

  • Which is shorter? The comprehension.

  • Which is clearer? For a simple transformation/filter, usually the comprehension.

  • Which would I prefer in production? The comprehension if the team can read it immediately.

  • Would the answer change for more complicated logic? Yes. As branching, side effects, or intermediate steps increase, an explicit loop is often clearer and easier to debug.

Readability is the deciding criterion, not line count.

Part 21 — Git checkpoint

From your Week 1 repository:

git status
git diff
git add .
git commit -m "Complete Week 2 data structures exercises"
git push

Align what you’ve done here with the Week 1 workflow:

Create → Explore → Save → Inspect → Commit → Push

Part 22 — Reflection

Answer these questions in a Markdown cell.

  1. What is the relationship between a variable name and a Python object?

  2. Why can mutability create unexpected behavior when passing objects to functions?

  3. Why would you choose a dictionary instead of a list?

  4. What does dynamically typed mean?

  5. What problem can type annotations help solve?

  6. When does a comprehension improve code?

  7. Why should a statistician care about Python’s object model and data structures?

Reference answers

  1. What is the relationship between a variable name and a Python object?
    A name is a reference to an object. The object has a type, value, and identity; the name can later be rebound to another object.

  2. Why can mutability create unexpected behavior when passing objects to functions?
    A function may receive a reference to the same mutable object held by the caller. If the function mutates it, the caller sees the change too.

  3. Why choose a dictionary instead of a list?
    Use a dictionary when values are naturally accessed by meaningful keys (for example, subject ID → group) rather than primarily by position.

  4. What does dynamically typed mean?
    Names are not permanently declared with one type; they can refer to objects of different types at runtime.

  5. What problem can type annotations help solve?
    They communicate intended interfaces, improve editor/static-analysis support, and help catch type mismatches before runtime when used with a type checker.

  6. When does a comprehension improve code?
    When a simple transformation/filter can be expressed more directly and readably than an explicit loop.

  7. Why should a statistician care about Python’s object model and data structures?
    Because aliasing, mutation, container choice, and data representation affect correctness, reproducibility, performance, and how safely statistical workflows manipulate data.

Completion checklist

  • Created Week 2 notebook/script

  • Inspected objects using type() and id()

  • Demonstrated shared references

  • Demonstrated mutation and rebinding

  • Explored mutable and immutable objects

  • Explored shallow copying

  • Used lists, tuples, dictionaries, and sets

  • Explained dynamic typing

  • Added type annotations

  • Created list comprehensions

  • Created dictionary comprehensions

  • Used comprehensions with conditions

  • Completed the participant data challenge

  • Compared loops with comprehensions

  • Created a statistical summary object

  • Committed Week 2 work to Git

  • Pushed Week 2 work to GitHub

What you should now understand

Python names
      ↓
Objects
      ↓
Identity + type + value
      ↓
References
      ↓
Mutability
      ↓
Data structures
      ↓
Comprehensions
      ↓
Readable computational code
      ↓
Pandas DataFrames next