Computer Programming 1 (Python) • Core Data Structures

List Mutability — I can explain mutability for lists and how in-place changes affect later code.

Understanding how Python lists change in place reveals why one edit can ripple through your entire program.

A Python coding terminal with a program flow and laptop.
Inside this lesson 10 sections 5 questions 5 figures

SECTION 1

Historical Context & Motivation

When programming languages were first being designed, engineers had to decide a fundamental question: when you change a piece of data, should the original value be modified directly, or should the language create a brand-new copy? This choice — whether data is mutable (changeable in place) or immutable (unchangeable once created) — has shaped how every modern language handles data structures.

Early languages like Fortran and C gave programmers raw access to memory, meaning almost everything was mutable by default. This was powerful but dangerous, because changing data in one part of a program could create hard-to-find bugs elsewhere. As languages evolved, designers began to separate mutable and immutable types intentionally to make code safer and easier to reason about.

1957 • Fortran & Raw Memory

Fortran gave programmers direct access to mutable arrays in memory, making in-place changes the default behavior for collections of data.

1972 • C Introduces Pointers

The C language made mutability explicit through pointers — variables that store memory addresses. Two pointers to the same array could cause unintended side effects, foreshadowing modern aliasing bugs.

1991 • Python Is Born

Guido van Rossum released Python with a clear distinction: lists are mutable, while strings and tuples are immutable. This design choice simplified data handling for beginners while retaining flexibility.

2000s • Functional Programming Revival

Languages like Haskell and Clojure popularized immutability by default, proving that avoiding in-place changes reduces bugs. Python kept its mutable lists but added immutable alternatives like tuples and frozensets.

The central question this lesson addresses is: when you modify a Python list, what actually happens in memory, and why can that single change affect other parts of your code? Answering this question is essential for writing correct, predictable programs.

SECTION 2

Core Principles of Mutability

To understand list mutability, you need to grasp a few foundational ideas about how Python manages data behind the scenes. Every value in Python lives at a specific location in memory, and variables are simply references — names that point to those memory locations. When you assign a list to a variable, the variable stores a reference (like an address) to the list object, not a separate copy of the list itself.

1

Mutable Object

An object whose contents can be changed after creation without creating a new object. Python lists, dictionaries, and sets are all mutable.
2

Immutable Object

An object that cannot be changed after creation. Strings, integers, floats, and tuples are immutable — any 'change' actually creates a brand-new object.
3

Reference (Alias)

A variable name that points to an object in memory. When two variables reference the same mutable object, changes through one name are visible through the other.
4

In-Place Modification

Changing the contents of an object at its existing memory address rather than creating a new object. Methods like .append(), .sort(), and index assignment modify lists in place.
5

Identity vs. Equality

Two lists can be equal (same contents) but not identical (different objects in memory). The 'is' keyword checks identity, while '==' checks equality.
✦

KEY TAKEAWAY

Think of a mutable list like a shared Google Doc. If you share the document link with a friend (creating an alias), any edits either of you make appear in the same document for both of you. An immutable object like a string is more like a printed handout — if you want to change it, you have to print a whole new copy.

SECTION 3

Visualizing References and In-Place Changes

The diagram below illustrates the most important — and most commonly misunderstood — behavior of mutable lists. When you write b = a, Python does not copy the list. Instead, both a and b become references — arrows pointing to the exact same list object in memory. Modifying the list through either name changes it for both.

Aliasing: Two Variables, One List ObjectSTEP 1: a = [10, 20, 30]aList object at id: 0x7f3a102030STEP 2: b = a (alias, NOT a copy)abSame list object at id: 0x7f3a102030STEP 3: b[0] = 99 (in-place change)abSame list — both see the change!992030print(a) → [99, 20, 30]print(b) → [99, 20, 30]Both changed!
In Step 1, variable a references a list. In Step 2, b = a creates a second reference (alias) to the same object. In Step 3, modifying the list through b changes what a sees too, because both arrows point to the same list in memory.

This is the core surprise for many new Python programmers. With immutable types like strings and integers, you never encounter this problem because any 'change' creates a new object. But with lists, the original object is modified directly — and every variable that references it will see the change. This is what makes understanding mutability so important.

SECTION 4

How Mutability Works Under the Hood

In Python, every object has three properties: a type (what kind of object it is), a value (its contents), and an identity (its unique address in memory, which you can check with the id() function). For mutable objects, the value can change while the identity stays the same. For immutable objects, the value can never change — any operation that seems to change it actually creates a new object with a new identity.

Mutable Operations vs. Reassignment

It is critical to distinguish between mutating a list and reassigning a variable. When you write a.append(40), you are reaching into the existing list object and adding an element — the identity stays the same, and all aliases see the change. When you write a = [1, 2, 3], you are pointing the variable a at a brand-new list object — the old list is unaffected, and any aliases still point to the original.

OperationCategoryChanges id()?Aliases Affected?
a[0] = 99In-place mutationNoYes — all aliases see it
a.append(40)In-place mutationNoYes — all aliases see it
a.sort()In-place mutationNoYes — all aliases see it
a = a + [40]Reassignment (new list)YesNo — aliases keep old list
a = sorted(a)Reassignment (new list)YesNo — aliases keep old list

Mutation vs. Reassignment: how each operation affects the list's identity and aliases

💡

The id() Test

You can verify mutability yourself. Run print(id(a)) before and after an operation. If the id stays the same, the list was mutated in place. If the id changes, a new object was created.

SECTION 5

Shallow Copies vs. Deep Copies

If aliasing is the problem, the solution is copying. When you want two independent lists, you need to create a true copy so that changes to one do not affect the other. Python provides two levels of copying: shallow copy and deep copy. For most cases in this course, a shallow copy is all you need.

Alias vs. Shallow Copy vs. Deep CopyALIAS: b = aab[10, 20, 30]ONE objectb[0] = 99a → [99, 20, 30]b → [99, 20, 30]⚠ Both changed!SHALLOW COPY: b = a[:]ab[10, 20, 30][10, 20, 30]TWO separate objectsb[0] = 99a → [10, 20, 30]b → [99, 20, 30]✓ Only b changed!COPY METHODSb = a[:]b = list(a)b = a.copy()All three create ashallow copy.For nested lists:import copyb = copy.deepcopy(a)This recursively copiesall inner objects too.
An alias (left) creates two names for one object — changes through either name affect both. A shallow copy (center) creates a new list with the same elements, so simple modifications are independent. The right panel lists common copy methods in Python.

A shallow copy creates a new list object and copies references to each element. For a list of simple values like integers or strings, this is perfectly safe. However, if your list contains other lists (a nested list), the inner lists are still shared between the original and the copy. In that case, you would need copy.deepcopy() to recursively copy everything, but that scenario is less common in an introductory course.

  • b = a[:] — slice the entire list to produce a shallow copy
  • b = list(a) — use the list constructor to build a new list from a
  • b = a.copy() — call the built-in copy method (Python 3.3+)

SECTION 6

Worked Example: Tracking a Mutable List Through Code

Let's trace through a realistic snippet of Python code step by step, paying close attention to which variables share the same list object and which have independent copies.

Tracing Aliases and Copies

  1. 1

    Step 1 — Create the original list

    We begin with scores = [85, 90, 78]. Python creates a new list object in memory and assigns the name scores to point at it.
    scores → [85, 90, 78] (let's say id = 1000)
  2. 2

    Step 2 — Create an alias

    We write backup = scores. This does NOT copy the list. The variable backup now points to the exact same object as scores. We can verify: scores is backup returns True.
    scores → [85, 90, 78] (id = 1000) | backup → same object (id = 1000)
  3. 3

    Step 3 — Create a true copy

    We write safe_copy = scores[:]. This creates a brand-new list object containing the same values. Now scores is safe_copy returns False, but scores == safe_copy returns True.
    safe_copy → [85, 90, 78] (id = 2000, different object)
  4. 4

    Step 4 — Mutate the original list

    We write scores[1] = 95. This is an in-place modification — the list at id 1000 now holds [85, 95, 78]. Since backup also points to id 1000, it sees the change. But safe_copy points to id 2000 and is unaffected.
    scores → [85, 95, 78] | backup → [85, 95, 78] | safe_copy → [85, 90, 78]
  5. 5

    Step 5 — Verify with print statements

    print(scores) outputs [85, 95, 78]. print(backup) outputs [85, 95, 78] — confirming the alias was affected. print(safe_copy) outputs [85, 90, 78] — confirming the copy was independent.
    Alias shares changes. Copy stays independent.

SECTION 7

Mutable vs. Immutable Types in Python

Python deliberately divides its built-in types into mutable and immutable categories. Understanding which types fall into each group helps you predict how your code will behave when you pass data to functions, store it in variables, or share it between different parts of a program.

TypeMutable?ExampleKey Behavior
listYes[1, 2, 3]Can append, remove, sort, or change elements in place
dictYes{"a": 1}Can add, remove, or update key-value pairs
setYes{1, 2, 3}Can add or discard elements
strNo"hello"Any 'change' creates a new string object
tupleNo(1, 2, 3)Cannot change after creation — like a frozen list
int / floatNo42, 3.14Reassignment creates a new number object

Common Python types organized by mutability

✦

KEY TAKEAWAY

Think of immutable types like your student ID number — once assigned, it can't be changed. If the school needs to give you a new one, they issue a completely new ID. A mutable list is more like a whiteboard in a shared study room: anyone who walks in can erase or add content, and everyone else who visits later sees those changes.

SECTION 8

Mutability and Functions: A Common Pitfall

One of the most common bugs in Python comes from passing a mutable list to a function. When you pass a list as an argument, the function's parameter becomes another alias to the same object. If the function modifies the list in place, the caller's list is changed too — even though the function looks like it's working with its own local variable. This is called a side effect.

ScenarioWhat HappensHow to Prevent Surprise
Function uses .append() on parameterCaller's list grows — side effectPass a copy: func(my_list[:])
Function uses .sort() on parameterCaller's list gets reordered — side effectUse sorted() instead (returns new list)
Function reassigns parameter with =Caller's list is NOT changed — reassignment rebinds the local nameNo issue — but function can't communicate result this way
Default mutable argument: def f(lst=[]):Default list is shared across calls — dangerous bug!Use None as default, create list inside function

How mutability interacts with function calls

As you move into more advanced programming — including object-oriented programming and data science with libraries like NumPy and Pandas — mutability becomes even more important. Large datasets are often stored in mutable structures for efficiency, so understanding when you're working with the original data versus a copy is critical to avoiding data corruption and bugs in real-world projects.

ℹ

Looking Ahead

In AP Computer Science and college courses, you will encounter the concept of pass-by-object-reference — Python's specific way of passing arguments. It is neither pure 'pass-by-value' nor 'pass-by-reference' (terms from other languages). Understanding list mutability now gives you the foundation to master that distinction later.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

In your own words, explain the difference between a mutable and an immutable object in Python. Give one example of each and describe what happens when you try to change them.
Show answer
A mutable object, such as a list, can have its contents changed in place — for example, my_list[0] = 99 modifies the existing list without creating a new object. An immutable object, such as a string, cannot be changed after creation. If you write my_string = my_string + "!", Python creates a brand-new string object and reassigns the variable to point to it. The key consequence is that multiple references to a mutable object all see changes, while immutable objects are 'safe' from aliasing surprises.

Question 2: BASIC

What is the output of the following code? x = [1, 2, 3] y = x y.append(4) print(x)
Show answer
The output is [1, 2, 3, 4]. When we write y = x, we create an alias — both x and y point to the same list object in memory. The .append(4) method mutates the list in place (it does not create a new list). Since x and y reference the same object, printing x shows the appended value too.

Question 3: INTERMEDIATE

Predict the output of this code and explain each result: a = [5, 10, 15] b = a[:] c = a a[0] = 99 print(a) print(b) print(c) print(a is c) print(a is b)
Show answer
The output is: [99, 10, 15] [5, 10, 15] [99, 10, 15] True False Variable b was created using slice notation a[:], which produces a shallow copy — a separate list object. Variable c was assigned directly from a, making it an alias. When a[0] is changed to 99, c reflects the change (same object), but b is unaffected (different object). The is operator confirms identity: a is c is True, a is b is False.

Question 4: APPLIED

A teacher writes a function to add bonus points to a list of student scores: def add_bonus(scores, bonus): for i in range(len(scores)): scores[i] += bonus return scores original = [70, 80, 90] updated = add_bonus(original, 5) print(original) print(updated) What does this print, and how could the teacher fix the function so that original is not accidentally modified?
Show answer
Both print statements output [75, 85, 95] because the function modifies the list in place. When original is passed to the function, the parameter scores becomes an alias for the same list. To fix this, the teacher should work on a copy inside the function: change the first line to scores = scores[:] or scores = list(scores). This way, the function creates an independent list, modifies the copy, and returns it — leaving original unchanged.

Question 5: CRITICAL THINKING

Consider this code: def mystery(data): data = data + [100] return data nums = [1, 2, 3] result = mystery(nums) print(nums) print(result) Explain why this function does NOT modify nums, even though lists are mutable. How does this differ from a function that uses data.append(100) instead? Connect your answer to the distinction between mutation and reassignment.
Show answer
The output is [1, 2, 3] for nums and [1, 2, 3, 100] for result. The expression data + [100] uses the + operator, which creates a brand-new list by concatenating the two lists. The assignment data = ... then rebinds the local variable data to point at that new list, leaving the original object untouched. This is reassignment, not mutation. By contrast, data.append(100) is mutation — it reaches into the existing list object and adds an element in place, which would affect nums because both names still point to the same object. The key insight is that the = operator changes what a variable references, while methods like .append() change the object itself.

SUMMARY

Lesson Summary

Summary

Python lists are mutable, meaning their contents can be changed in place without creating a new object. When you assign one variable to another with b = a, both variables become aliases — references pointing to the same list object in memory. Any in-place modification (such as .append(), .sort(), or index assignment) through one alias will be visible through every other alias to the same object.

To create an independent copy, use shallow copy techniques such as a[:], list(a), or a.copy(). Always distinguish between mutation (changing the object) and reassignment (pointing a variable at a new object). When passing lists to functions, remember that the parameter is an alias — if the function mutates it, the caller's data changes too. Mastering these concepts now will prevent some of the most frustrating bugs you'll encounter as a programmer.

Varsity Tutors • Computer Programming 1 (Python) • List Mutability