Wednesday, September 23rd, 2026¶

Last class, we discussed list slicing and list comprehension. In particular, we can generate slices of a list in the following ways:

  • my_list[start_index:] will start the slice at start_index and go the end of the list.
  • my_list[:stop_index] will start the slice at the beginning of the list (i.e. index 0) and proceed until one less than stop_index.
  • my_list[start_index:stop_index] will start the slice at start_index and proceed until one less than stop_index.
  • my_list[::skip] will start the slice at the beginning of the list and go to the end, but will go in steps of skip.
  • my_list[start_index:stop_index:skip] will start at start_index, proceed in steps of skip, and will stop when the index is greater than or equal to stop_index.

We can use list comprehension in the following ways:

  • [<some expression> for <some item> in <some iterable>] will generate a list by looping through each element of <some iterable>, assigning the element to the variable name <some item>, and storing the result of <some expression> in the list.
  • [<some expression> for <some item> in <some iterable> if <some condition>] will work similarly, except the result of <some expression> will only be stored in the list of <some condition> is True.

Exercise: Use list comprehension to generate a list cubes containing the cubes of the first $40$ positive integers.

In [1]:
cubes = []
for i in range(1,41):
    cubes.append(i**3)
In [3]:
cubes = [i**3 for i in range(1,41)]
In [4]:
print(cubes)
[1, 8, 27, 64, 125, 216, 343, 512, 729, 1000, 1331, 1728, 2197, 2744, 3375, 4096, 4913, 5832, 6859, 8000, 9261, 10648, 12167, 13824, 15625, 17576, 19683, 21952, 24389, 27000, 29791, 32768, 35937, 39304, 42875, 46656, 50653, 54872, 59319, 64000]

Exercise: Use list slicing to print out the third through tenth of these cubes (i.e. index-2 through index-9).

In [5]:
print(cubes[2:10])
[27, 64, 125, 216, 343, 512, 729, 1000]
In [ ]:
 

Exercise: Use list slicing to print out the last ten cubes from the cubes list.

In [6]:
print(cubes[30:])
[29791, 32768, 35937, 39304, 42875, 46656, 50653, 54872, 59319, 64000]
In [7]:
print(cubes[-10:])
[29791, 32768, 35937, 39304, 42875, 46656, 50653, 54872, 59319, 64000]
In [8]:
print(cubes[len(cubes)-10:])
[29791, 32768, 35937, 39304, 42875, 46656, 50653, 54872, 59319, 64000]

Exercise: Use list comprehension to generate a list containing, out of the cubes of the first $40$ positive integers, only those that are divisble by $6$.

In [9]:
cubes_div_by_6 = []
for cube in cubes:
    if cube % 6 == 0:
        cubes_div_by_6.append(cube)
In [10]:
cubes_div_by_6 = [cube for cube in cubes if (cube % 6 == 0)]
In [11]:
print(cubes_div_by_6)
[216, 1728, 5832, 13824, 27000, 46656]

Mutable and immutable objects¶

The word mutable means "liable to change." Consider the following experiments in which we explore how Python handles changing variables.

Experiment #1:

In [12]:
n = 3
m = n

print('n = {}'.format(n))
print('m = {}'.format(m))
n = 3
m = 3

What happens to m if we make changes to n?

In [13]:
n = n + 5

print('n = {}'.format(n))
print('m = {}'.format(m))
n = 8
m = 3

We see that m is unchanged when n is changed.

Experiment #2:

In [14]:
list1 = [1,2,3,4]
list2 = list1

print('list1 =',list1)
print('list2 =',list2)
list1 = [1, 2, 3, 4]
list2 = [1, 2, 3, 4]

What happens to list2 if we make changes to list1?

In [15]:
list1[0] = 99

print('list1 =',list1)
print('list2 =',list2)
list1 = [99, 2, 3, 4]
list2 = [99, 2, 3, 4]

This time, we see that list2 is changed when list1 is changed.

What's going on here? Setting m = n did not cause changes in n to propogate back to m, but in the list example, setting list2=list1 did cause changes to list1 to propogate to list2.

The difference is that integers are immutable objects in Python while lists are mutable obects.

  • Immutable objects:
    • Integers
    • Strings
    • Floats
    • Booleans
    • Tuples
  • Mutable objects:
    • Lists
    • Dictionaries
    • Sets
    • NumPy arrays

For mutable objects, variable assigments point to a place in memory that stores the obect (in this case a list). That is, list1 and list2 point to the same location in memory. Making modifications to the object modifies the values stored in this shared memory location, so all variables pointing to this location are affected. We can use the id function to get an identifier for the memory location of an object.

In [16]:
id(m)
Out[16]:
140711861531624
In [18]:
id(n)
Out[18]:
140711861531784
In [19]:
id(list1)
Out[19]:
2279988316160
In [20]:
id(list2)
Out[20]:
2279988316160

We can check whether two variables have the same ID to see whether they share the same memory location (so that changes to one affect the other).

In [21]:
id(list1) == id(list2)
Out[21]:
True
In [23]:
list1 = [1,2,3,4]
list2 = list1
list3 = [1,2,3,4]
In [24]:
id(list1)
Out[24]:
2279987984768
In [25]:
id(list2)
Out[25]:
2279987984768
In [26]:
id(list3)
Out[26]:
2279988362752
In [27]:
list1[0] = 99
print(list1)
print(list2)
print(list3)
[99, 2, 3, 4]
[99, 2, 3, 4]
[1, 2, 3, 4]
In [28]:
list3.append(5)

print(list1)
print(list2)
print(list3)
[99, 2, 3, 4]
[99, 2, 3, 4]
[1, 2, 3, 4, 5]
In [29]:
id(list3) == id(list1)
Out[29]:
False
In [ ]:
 

Alternatively, we can use the is operator to check if two variables refer to the same object in memory.

In [30]:
list1 is list2
Out[30]:
True
In [31]:
list1 is list3
Out[31]:
False
In [ ]:
 
In [32]:
list1 = [1,2,3,4]
list2 = list1
list1 = [5,6,7,8]

print(list1)
print(list2)
[5, 6, 7, 8]
[1, 2, 3, 4]
In [33]:
list1 is list2
Out[33]:
False
In [34]:
list1 = [1,2,3,4]
list2 = list1
list1[:] = [5,6,7,8]

print(list1)
print(list2)
[5, 6, 7, 8]
[5, 6, 7, 8]

Sometimes, we may want to copy a list to a new variable but sever this link. That is, we might want a new variable (pointing to a new location in memory) that has the same values. To do this, we can use the .copy() method:

In [37]:
list1 = [1,2,3,4]
list2 = list1.copy()

print(list1)
print(list2)

list1 is list2
[1, 2, 3, 4]
[1, 2, 3, 4]
Out[37]:
False
In [38]:
list1.append(5)

print(list1)
print(list2)
[1, 2, 3, 4, 5]
[1, 2, 3, 4]

More about function inputs¶

Positional and keyword arguments¶

So far, we've discussed defining functions that take in some number of input variables. For example, let's define a function y that takes in four inputs $t$, $a$, $b$, $c$, and evalutates the quadratic $at^2 + bt + c$.

In [39]:
def y(t,a,b,c):
    return a*t**2 + b*t + c

When we call a function, we can simply enter values in the corresponding order to map them to the input variables. For example, calling y(0,1,2,3) will execute the function by mapping t=0, a=1, b=2, and c=3.

In [40]:
y(0,1,2,3)
Out[40]:
3

Sometimes, it is useful to explicitly assign values to variables. This can be done using keyword arguments. To use a keyword argument, we plug a mapping <variable name> = <value> into the function.

For example, calling y(t=0, a=1, b=2, c=3) will explicitly map t=0, a=1, b=2, and c=3 and then execute the function's code.

In [41]:
y(t=0, a=1, b=2, c=3)
Out[41]:
3
In [ ]:
 

With keyword arguments, we do not need to enter the values in the same order that was used to define the function. That is, y(a=1, b=2, c=3, t=0) will give the same result as y(0,1,2,3), whereas y(1,2,3,0) would map t=1, a=2, b=3, c=0.

In [42]:
y(c=3, a=1, b=2, t=0)
Out[42]:
3
In [ ]:
 

When calling a function, inputs that we've not explicitly labeled as keyword arguments are called positional arguments. All positional arguments must come before any keyword arguments, and their ordering determines the mapping to the input variable names.

In [43]:
y(0, 1, c=3, b=2)
Out[43]:
3
In [44]:
y(t=0, a=1, 2, 3)
  Cell In[44], line 1
    y(t=0, a=1, 2, 3)
                    ^
SyntaxError: positional argument follows keyword argument
In [ ]:
 

Default parameters¶

It often happens that we write a function that takes in an input variable that has a natural or typically useful value. For example, suppose we want to use the y function to model an object in freefall due to gravity using the quadratic expression,

$$y(t) = at^2 + bt + c.$$

Note: the acceleration of the object is $y''(t) = 2a$ and the initial velocity is $y'(0) = b$. It might be typical that we are considering an object in freefall on Earth, where the acceleration due to gravity is approximately $-9.8\,m/s^2$ (or $a \approx -4.9$). We also might be often interested in modeling an object that is released from rest (with zero initial velocity, or $b=0$).

In situations like this, we can assign a default value to the input variables a and b when we define the function t. To do so, we use the syntax def <some function>(<some variable>=<default value>):. Let's modify the y function to default to a=-4.9 and b=0.

Note: When defining a function that takes in several variables, any variables that have assigned default values must come after all variables that do not. In this case, let's re-order the inputs to (t,c,b,a) (since we wish to assign default values for a and b).

In [52]:
def y(t,c,a=-4.9,b=0):
    return a*t**2 + b*t + c
In [53]:
y(10,1000) # Height of an object 10 seconds after falling from 1000m on Earth
Out[53]:
509.99999999999994
In [54]:
y(10,1000, a=-3.7/2) # Height of an object 10 seconds after falling from 1000m on Mars
Out[54]:
815.0

Given the physical model, it might be a good idea to use better names than a, b, c. For example, we could rephrase the function parameters in terms of the initial velocity $v_0$ and the initial height $y_0$.

In [56]:
def y(t,y0,g=-9.8,v0=0):
    return (g/2)*t**2 + v0*t + y0
In [57]:
y(10, 1000, g=-3.7, v0=100)
Out[57]:
1815.0

Given this function, it would be nice if we had a way to visualize the paths taken by objects in freefall. This leads us to...

Plotting in Python¶

We will the pyplot submodule from the matplotlib module for our plotting needs. When importing modules (or submodules), we can assign our own short-hand name using the syntax import <some module or submodule> as <some short-hand name>.

Typically, we will import the matplotlib.pyplot module and assign it the short-hand name plt.

In [58]:
import matplotlib.pyplot as plt

The pyplot module includes many functions for data visualization and plotting. The most basic is the plot function.

In [60]:
#help(plt)
In [62]:
#help(plt.plot)

The plot function is extremely flexible in how it can be called (by making use of default parameters and keyword arguments). One of the most basic ways to use the plot function is to enter a list of $x$-values and a list of $y$-values as inputs. That is, plt.plot(<list of x-values>, <list of y-values>).

In [63]:
x_list = [i for i in range(1,11)]
y_list = [x**2 for x in x_list]
In [64]:
plt.plot(x_list, y_list)
Out[64]:
[<matplotlib.lines.Line2D at 0x212dbc1afd0>]
No description has been provided for this image

Exercise: Use the function y defined previously to plot the height over time of an object dropped (from rest) from a height of $1,000$ meters (on Earth) for the range $0 \leq t \leq 16$.

In [65]:
t_list = [t for t in range(17)]
y_list = [y(t, 1000) for t in t_list]
In [66]:
plt.plot(t_list, y_list)
Out[66]:
[<matplotlib.lines.Line2D at 0x212dceaa710>]
No description has been provided for this image

We can call plt.plot several times within a cell to plot several pieces of data.

Exercise: Replicate the previous plot, then add another curve that shows the height over time of the same object dropped on Mars, where the acceleration due to gravity is approximately $-3.71\, m/s^2$.

In [74]:
y0 = 1000      # Initial height
g_Earth = -9.8 # Gravitational constant on Earth
g_Mars = -3.7  # Gravitational constant on Mars

t_list = [t for t in range(17)]
y_Earth_list = [y(t, y0, g_Earth) for t in t_list]
y_Mars_list = [y(t, y0, g=g_Mars) for t in t_list]
In [75]:
plt.plot(t_list, y_Earth_list)
plt.plot(t_list, y_Mars_list)
Out[75]:
[<matplotlib.lines.Line2D at 0x212ddcbdd10>]
No description has been provided for this image