my_list = [1, 2, 3]
print(my_list)[1, 2, 3]
Some Python objects can change after creation. Others cannot.
This distinction becomes especially important when objects are passed to functions.
| Data type | Mutable? |
|---|---|
| List | Yes |
| Dictionary | Yes |
| Set | Yes |
| Integer | No |
| Float | No |
| Boolean | No |
| String | No |
| Tuple | No |
A function can modify a mutable object received as an argument. The caller will see that change.
The append() method changes a list in place.
my_list = [1, 2, 3]
print(my_list)[1, 2, 3]
my_list.append(4)
print(my_list)[1, 2, 3, 4]
Many methods that mutate a list return None.
my_list = [1, 2, 3]
result = my_list.append(4)
print(result)None
print(my_list)[1, 2, 3, 4]
The assignment below is therefore a common mistake:
my_list = [1, 2, 3]
my_list = my_list.append(4)
print(my_list)None
append() changes the original list and returns None. The assignment then replaces the variable with None.
Not every method mutates its object. For example, string.upper() returns a new string because strings are immutable. Check what a method returns before assigning its result.
Compare a mutating list method with list concatenation.
append()original = [1, 2, 3]
new_list = original.append(4)
print(original)[1, 2, 3, 4]
print(new_list)None
original changes, while new_list receives None.
+original = [1, 2, 3]
new_list = original + [4]
print(original)[1, 2, 3]
print(new_list)[1, 2, 3, 4]
The + operation creates a new list. The original list remains unchanged.
Without running the code, determine the value printed by each cell.
playlist = ["Intro", "Night Drive"]
result = playlist.append("Home")print(playlist)print(result)The first cell prints:
['Intro', 'Night Drive', 'Home']
The second cell prints:
None
When a function receives a mutable object, its parameter refers to the same object used by the caller.
def add_item(items: list, item) -> None:
items.append(item)shopping_list = ["bread", "coffee"]
print(shopping_list)['bread', 'coffee']
returned_value = add_item(shopping_list, "apples")
print(returned_value)None
print(shopping_list)['bread', 'coffee', 'apples']
The function returns None, but shopping_list still changes.
def add_attendee(attendees: set, name: str) -> None:
attendees.add(name)event_attendees = {"Maya", "Leo"}
add_attendee(event_attendees, "Nora")
print(event_attendees){'Leo', 'Nora', 'Maya'}
def update_status(record: dict, status: str) -> None:
record["status"] = statusproject = {"name": "Website", "status": "planned"}
update_status(project, "active")
print(project){'name': 'Website', 'status': 'active'}
Predict the values of guest and returned_value after the function call.
def add_preference(profile: dict, preference: str) -> None:
profile["preference"] = preference
guest = {"name": "Ava"}
returned_value = add_preference(guest, "window seat")print(guest)print(returned_value)guest contains both "name" and "preference". returned_value is None because the function has no return statement.
Assigning a new object to a parameter only changes the local parameter. It does not replace the caller’s object.
def replace_list(items: list) -> None:
items = ["replacement"]original = ["first", "second"]
replace_list(original)
print(original)['first', 'second']
The function rebinds its local variable items. It does not mutate original.
Compare that with slice assignment, which changes the existing list:
def replace_contents(items: list) -> None:
items[:] = ["replacement"]original = ["first", "second"]
replace_contents(original)
print(original)['replacement']
If a function should leave its input unchanged, create a copy before modifying it.
def add_item_safely(items: list, item) -> list:
copied_items = items.copy()
copied_items.append(item)
return copied_itemsshopping_list = ["bread", "coffee"]
updated_list = add_item_safely(shopping_list, "apples")
print(shopping_list)['bread', 'coffee']
print(updated_list)['bread', 'coffee', 'apples']
You can copy dictionaries and sets in the same way.
def change_city(profile: dict, city: str) -> dict:
copied_profile = profile.copy()
copied_profile["city"] = city
return copied_profileprofile = {"name": "Maya", "city": "Madrid"}
updated_profile = change_city(profile, "Lisbon")
print(profile){'name': 'Maya', 'city': 'Madrid'}
print(updated_profile){'name': 'Maya', 'city': 'Lisbon'}
Complete add_tag_safely() so it returns an updated set without changing the original set.
def add_tag_safely(tags: set, new_tag: str) -> set:
# Complete the functiondef add_tag_safely(tags: set, new_tag: str) -> set:
copied_tags = tags.copy()
copied_tags.add(new_tag)
return copied_tagsThe .copy() method creates a shallow copy. The outer object is new, but nested mutable objects are still shared.
original = [["Monday"], ["Tuesday"]]
copied = original.copy()
copied[0].append("Python")
print(copied)[['Monday', 'Python'], ['Tuesday']]
print(original)[['Monday', 'Python'], ['Tuesday']]
Both outputs include "Python" because the inner lists are shared.
Use deepcopy() when nested mutable objects must also be copied.
from copy import deepcopy
original = [["Monday"], ["Tuesday"]]
copied = deepcopy(original)
copied[0].append("Python")
print(copied)[['Monday', 'Python'], ['Tuesday']]
print(original)[['Monday'], ['Tuesday']]
Will original contain "dessert" after this code runs? Explain your answer.
original = {
"starter": ["soup"],
"main": ["pasta"],
}
copied = original.copy()
copied["main"].append("dessert")Yes. The dictionaries are different objects, but both refer to the same nested list stored under "main". Appending to that list affects both dictionaries.
Default parameter values are created once, when Python defines the function. A mutable default is therefore shared by later calls.
def add_to_list(value, stored_values: list = []) -> list:
stored_values.append(value)
return stored_valuesfirst_result = add_to_list(1)
print(first_result)[1]
second_result = add_to_list(2)
print(second_result)[1, 2]
separate_result = add_to_list(3, stored_values=[])
print(separate_result)[3]
fourth_result = add_to_list(4)
print(fourth_result)[1, 2, 4]
The first, second, and fourth calls use the same default list. The third call receives a separate empty list.
The same problem affects dictionary and set defaults.
def remember_tag(tag: str, tags: set = set()) -> set:
tags.add(tag)
return tagsprint(remember_tag("python")){'python'}
print(remember_tag("data")){'python', 'data'}
Avoid mutable default values such as [], {}, and set().
How many keys does animal_sounds contain at the end? Explain why.
def add_entry(key, value, records: dict = {}) -> dict:
records[key] = value
return records
clothes = add_entry("shirt", "blue")
clothes = add_entry("pants", "green", clothes)
animal_sounds = add_entry("dog", "woof")
animal_sounds = add_entry("cat", "meow", animal_sounds)print(animal_sounds)animal_sounds contains four keys: "shirt", "pants", "dog", and "cat".
The first and third calls use the same default dictionary. The second and fourth calls receive that same dictionary explicitly.
Use None as the default. Create a new mutable object inside the function when needed.
def add_to_list(value, stored_values: list = None) -> list:
if stored_values is None:
stored_values = []
stored_values.append(value)
return stored_valuesEach call without a list now creates a separate list.
print(add_to_list(1))[1]
print(add_to_list(2))[2]
print(add_to_list(3))[3]
You can still provide an existing list when you want the function to update it.
saved_values = [10]
result = add_to_list(20, saved_values)
print(result)[10, 20]
print(saved_values)[10, 20]
Rewrite add_entry() so calls without a dictionary do not share data.
def add_entry(key, value, records: dict = {}) -> dict:
records[key] = value
return recordsdef add_entry(key, value, records: dict = None) -> dict:
if records is None:
records = {}
records[key] = value
return recordsWrite a function named enable_notification() with these requirements:
def enable_notification(name: str, preferences: set = None) -> set:
if preferences is None:
preferences = set()
preferences.add(name)
return preferences.copy() is shallow, so nested mutable objects remain shared.None as the default and create the mutable object inside the function.When you feel ready, finish the Module 2 homework exercises available here. To earn participation credit, complete the exercises highlighted in red.