a = [1, 2, 3]
b = a
print(f"a = {a}, b = {b}")
print(f"a == b ? {a == b}")INSEA Techniques de réduction de dimension - 2025
TP 1: Python basics: types, references and mutability
Author: Hicham Janati
How to follow this lab:
- The goal is to understand AND retain in the long term: resist copy-pasting, prefer typing manually.
- Getting stuck while programming is completely normal: search online, use documentation, or use the AI.
- When prompting the AI, you must be specific. Explain that your goal is to learn, not to get an instant solution no matter what. Ask for short, explained answers with alternatives.
- NEVER ASK THE AI TO PRODUCE MORE THAN ONE LINE OF CODE!
- Adopt the
Solve-Itmethod: always try to solve a question or predict the output of code before running it. Learning happens when you confirm your understanding—and even more when you’re wrong and surprised.
I - Python basics
1 - Variables, names and identity
In Python, variable names are references to objects that live in memory. The identity of an object refers to its unique location in memory.
1.1 The identity of a variable:
Question 1: Predict the output of:
The variables a and b do not “contain” the list. Instead, they are names pointing to the same object. We can confirm this by running the id function which returns the memory location:
print(f"id(a) = {id(a)}, id(b) = {id(b)}")1.2 Comparing with == or is:
Equality (==) checks if two objects have the same value
Identity (is) checks if two names refer to the same object
Question 2: Predict the output of:
a = [1, 2, 3]
b = [1, 2, 3]
print(f"a = {a}, b = {b}")
print(f"a == b ? {a == b}")
print(f"a is b ? {a is b}")
print(f"id(a) = {id(a)}, id(b) = {id(b)}")Question 3
What does the following code change internally ?
a = 10
a = 201.3 Mutable / immutable types
Some python types are immutable: once they’re created, their values cannot be changed without creating a new object: - ints, floats, booleans, strings, tuples.
Other can be changed: - dicts, lists, set
Analyse what happens in these two snippets of code:
a = 10
a = a + 5The operation x = x + 5 appears to change the value of x, but it actually computes 10 + 5, saves it in a new physical location and attaches the name a to it.
x = [5, 4]
x.append(1)The operation x.append(1) is said to “modify” (mutate) the object “in-place”: it changes the value of the memory location.
Try this with strings:
s = "something"
s = s.upper()Question 4: what is the output of the following cells ? analyse with id
a = [1, 2, 3]
b = a
a.append(15)
print(f"a = {a}, b = {b}")a = [1, 2, 3]
b = a
a = a + [15]
print(f"a = {a}, b = {b}")a = [1, 2, 3]
b = a
a += [15]
print(f"a = {a}, b = {b}")a = [2, 3, 4]
b = a
b[0] = 1000The operation += is also said to be in-place: when manipulating lists, always be careful to what you’re actually changing !
Question 5: What about slicing ?
a = [0, 1, 2, 3, 4, 5, 6]
b = a[:4]
b[0] = 1000Let’s say I want to create a copy b of a list a in a different physical location (so that changingb won’t affect a) how can I do it, based on the previous question ?
Question 6: try these operations seen on list to tuples:
a = (1, 2, 3)
b = a
a = (15, 2)a = (1, 2, 3)
b = a * 2
print(f"a = {a}, b = {b}")What if we put a mutable type inside an immutable type ?
a = (1, [1, 2, 3])
b = a
a[1].append(1000)
print(f"a = {a}, b = {b}")We want to create a copy of a list using a slice like before, but the list contains another list. Would it work ?
a = [1, 2, 3, [4, 5, 6]]
b = a[:]
b[3].append(1000)
print(f"a = {a}, b = {b}")
The slice copies the references of the elements of the list but NOT recursively: it will not go through the elements of the elements (if any are lists). Such a recursive copy is called a deepcopy which can be done by the package copy:
import copy
a = [1, 2, 3, [4, 5, 6]]
b = copy.deepcopy(a)
b[3].append(1000)
print(f"a = {a}, b = {b}")1.4 What happens inside a function call
Question 7: Call this function with different types, does the function make a copy of the object ?
def return_self(x):
print(f"inside function: x = {x}, id(x) = {id(x)}" )
return xQuestion 8: Same for this one which manipulates an immutable int. Is the returned object the same ?
def add_one(x):
print(f"inside function: x = {x}, id(x) = {id(x)}" )
x = x + 1
print(f"inside function after increment: x = {x}, id(x) = {id(x)}" )
return x
a = 10
b = add_one(a)Question 9: What about mutable types like lists ?
def append_one(x):
print(f"inside function before append: x = {x}, id(x) = {id(x)}" )
x.append(1)
print(f"inside function after append: x = {x}, id(x) = {id(x)}" )
return x
a = [1]
b = append_one(a)The logic we discovered earlier happens inside the function, Python never makes copies when passing arguments to functions: the references of the objects are passed as if the code is executed outside the function. The local variables are “local names” attached to existing objects.
Question 10: What about this one which manipulates a list ?
def change_list(x):
x = [4, 5, 6]
return xQuestion 11: Explain this behavior
def change_list(x=[]):
x.append(1)
return x
change_list()
change_list()
change_list()1.5 Variables scopes
When Python looks up a name, it searches in this order, the LEGB order:
- Local – inside the current function (including function arguments).
- Enclosing – outer function scopes (for nested functions, function inside a function).
- Global – module-level names.
- Built-in – stuff like
len,print,int,def.
Question 12: What is the output of the following cells ?
def replace_list(lst):
lst = [99, 100]
x = [1, 2, 3]
replace_list(x)
print(x)x = 10
def outer():
x = 20
def inner():
x = 30
print(x)
inner()
outer()