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-It method: 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:

a = [1, 2, 3]
b = a

print(f"a = {a}, b = {b}")

print(f"a == b ? {a == b}")

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 = 20

1.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 + 5

The 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] = 1000

The 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] = 1000

Let’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 x

Question 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 x

Question 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:

  1. Local – inside the current function (including function arguments).
  2. Enclosing – outer function scopes (for nested functions, function inside a function).
  3. Global – module-level names.
  4. 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()