Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
explore-for-students
GitHub Repository: explore-for-students/python-tutorials
Path: blob/main/Lesson 1 - Python basics.ipynb
968 views
Kernel: Python 3 (ipykernel)

Lecture 1: Python basics

Authors:

  • Yilber Fabian Bautista

  • Keiwan Jamaly

  • Sean Tulin

Objectives:

By the end of this lecture you will be able to:

  • Be familiar with Jupyter notebooks

  • Define variables and differentiate between global and local variables.

  • Identify and use different object types in python.

  • Use some of the python's default functions and define your own functions.

  • Manipulate lists

  • Introduction to numpy and matplotlib libraries

Welcome to Jupyter notebooks

Jupyter notebooks are an easy way to write Python code. Here you don't need to write your entire code at once, but you can run it one part at a time (or line-by-line if you want). This is helpful for debugging since you can make sure each step works before you go to the next step of your code.

Your code is run in a code cell, which looks like this:

You can type as many lines of code as you want in a code cell (press <enter> to go to a new line). Once you are done, press <shift-enter> (or click the Run button above). The final line of your code will be printed as an output (if there is something to print).

Python is not a compiled language, so there is no step where you need to compile your code before you run it.

Comments

It is good practice to write comments in your code. All text beginning with # is commented out. Comments can even follow your code on the same line.

# This is a comment x = 5 # This is another comment

Variables

When programming, generally one wants to implement algorithms that simplify cumbersome calculations. Let us consider the following analytic expression:

x2+1\begin{equation*} x^2 + 1 \end{equation*}

In this expression, xx corresponds to a variable, whereas 11 is a fixed number. To define a variable in a python program we use the following syntax:

x = 5
x = 5

This simply means that every time in the code we use x , the code automatically replaces it by its numerical value. For instance, going back to our example analytic example, if we evaluate

x**2 + 1

we will get as output 26 . Try this out by yourself

# Type 'x**2 + 1' below and push <shift-enter> to evaluate

Variables defined in one cell (such as x above) are defined in all cells. This is a global variable. Every time we use x in any cell in the same notebook, python automatically assigns its numeric value. Below, we will see examples of local variables where this is no longer true.

# Evaluate x in this cells and convince yourself that it is a global variable

Types

In mathematics, a variable can represent different types of objects, e.g., natural numbers, real numbers, vectors, matrices, etc. It is the same in programing. The objects have defined types. There are many types in programing, and you can even define your own, but the most important ones you need to know for the moment are (without any particular order):

  • Boolean point number (bool) : either True or False (note capitalization is important!)

  • Integer (int): a number; 1, 4, 6, 23, also -34

  • Floating Point Number (float): a decimal number; 2.4, 34.2325, -54.234967

  • String (str): a sequence of characters; "hello", "I learn python!", "I am a string"

Python is a dynamically-typed language. Python tries to guess the type of a variable when you assign it a value, and you can change the type of a variable at any time.

Let us explore these examples in practice.

Boolean variables

a = True print('type of a:',type(a))
type of a: <class 'bool'>

Integer and float variables

x = 5 print("type of x:", type(x)) y = 5.0 print("type of y:", type(y))
type of x: <class 'int'> type of y: <class 'float'>

String variables

r = 'hello world' print(" type of r: ", type(r))
type of r: <class 'str'>

Different variable types have different kinds of operations, for example:

  • Integers and floats can be added 1 + 2 or multiplied 1 * 2.

  • Strings can be concatenated using the + operator, e.g., "hello" + "world". You can also do string comparison using "hello" == "hello".

You can read more about them here, here and here. But let us see an specific example of how to concatenate two strings.

string1 = "hello" string2 = 'world' string3 = string1 + string2 print(string3)
helloworld

String comparison returns output of boolean type:

print(string1 == string2) print(type(string1 == string2))
False <class 'bool'>

Strings are just sequences of characters. You can extract one specific character by indexing using the following notation: for example,

string1[0]

returns the first character of string1, i.e., the letter h. Note that Python starts counting at zero.

You can extract a substring by slicing. The notation is string1[i:j] will return characters from string1 from character i up to but not including character j. For example, the slice string3[3:8] returns the substring "lowor". The notation string1[i:j:k] will include only every kth character. So, string3[3:8:2] will return "lwr" since every other character is skipped.

You can also count backwards from the end of the string by setting the index to be -1.

Try the following examples and feel free to change them to see how it works.

print(string3[6]) # The 7th element of the string print(string3[-1]) # The last element of the string print(string3[3::3]) # The 4th element through the end of the string, keeping only every 3rd character print(string3[::-1]) # Entire string backwards.
o d lod dlrowolleh

A variable of a given type can be turn into a variable of a different type. For instance, a float can be turn into an integer using the python's default function int(). An integer can be turn into an string using the function str(). In a similar way, you can ask for the integer value of the division of two numbers in two ways, for instance doing the usual division and the using the int() function on the final result. You can also use the command a//b. Let see

x = 5 # this is an integer x = float(x) # this is a float using float() function print("type of x:", type(x)) print(int(9.1)) #use int() function print(type(str(1))) #use str() function print(int(9/2)) print(9//2) #ask for the integer part of a division
type of x: <class 'float'> 9 <class 'str'> 4 4

Operations can also change the type of a variable, for instance, the division of two integers numbers give in general float numbers

x = 5 print("x = 5 is of type :", type(x)) y = x/3 print("y = x/3 is of type :", type(y))
x = 5 is of type : <class 'int'> y = x/3 is of type : <class 'float'>

There are is a set of mathematical operations which are applicable to the different variable types, "+, -, *, /, **, //, %, <, >, <=, >=, ==, !=". The order of operations can be seen here. But you can always override the priority with round brackets.

print(3 + 3) print(3 + 3 * 15) print((3 + 3) * 15)
6 48 90

To finalize this section, let us point that all of the variables that we have defined above are Global variables, that means that we can use them in different cells in the notebook without having to define them again. For instance we can print the value of x defined above.

x
5

Evaluate some of the previous defined variables to convince yourself that all variables defined so far are global.

Functions

Going back to our mathematical expression

x**2 + 1

we now want to evaluate it at an specific value, for instance x = 3. The result of this evaluation will be 10

x = 3 x**2 + 1
10

We could do this also for another input number, for example x = 7.

x = 7 x**2 + 1
50

or x = 50

x = 50 x**2 + 1
2501

You get the idea.. You can save your time by abstracting

x**2 + 1

into a function which can be reused any time we desire to. The syntax for defining a function in python is the following:

def function_name(x1): # ...

Here

x1 is a function parameter (independent variable).

It is recommended that you use functions names that accurately describe the function purpose.

You can have multiple function parameters.

def f(x1, x2): # ...

The function parameters x can be of any type (including functions themselves).

A function generally takes an input and returns an output. Let us imagine we want to write a function that adds two parameters and squares them afterwards. You could write a code of the following form

def sum_and_square(x1, x2): power = 2 s = x1 + x2 s_sq = s**power return s_sq

We can now use this function with specific input variables as follows

x = sum_and_square(2, 3) x
25

Let us take this example function to discuss local variables. As you can see, inside the function we have defined the variable power = 2, that means that inside the function, anywhere you encounter the variable power, the program automatically replace its value 2. However, outside the function the variable power is not defined, and if you try to call it, you will get an error message. power was written as a local variable.

power
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Input In [19], in <cell line: 1>() ----> 1 power NameError: name 'power' is not defined

Python syntax for error printed is very informative, as we can see, it tells that the variable power has not been defined.

Returning to our function. As you can see, the job of return is to store the result of the operation in the function, in the variable x. A function doesn't need to have a return statement, for example, when you work with classes (you will learn about classes later).

Keep in mind, that the argument's types are carried into the function, so you get an error, when you try use undefined operations between different type objects. For example, the sum of a string and a number is not defined, and the error would be the following

sum_and_square("hello", 2)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Input In [20], in <cell line: 1>() ----> 1 sum_and_square("hello", 2) Input In [17], in sum_and_square(x1, x2) 1 def sum_and_square(x1, x2): 2 power = 2 ----> 3 s = x1 + x2 4 s_sq = s**power 5 return s_sq TypeError: can only concatenate str (not "int") to str

Let us finally comment on a useful feature when defining functions. You can pre-assign a default value for the independent variables. For instance, let us imagine we want to define a function of two parameters a, b, but we want to specify a default value for b=1, so that when using the function, it assumes that b =1, but we could change the value of b if desired. The syntax for this will be

def fun(a,b=1): # ...

Here we refer to b as a named variable. Let us see how this works in the following example

def fun(a,b=1): return a + b
print(fun(3)) # use the default value for variable b print(fun(3,b=2)) # Change the input value for b
4 5

Notice that the order of the arguments matters, unnamed variables always go first, and named variables go afterwards. For instance, if you want to invert the order, you will get an error message

fun(b=1,3)
Input In [23] fun(b=1,3) ^ SyntaxError: positional argument follows keyword argument

Defining function with default key-values for different parameters is useful when dealing functions with a big number of arguments, since it allows us to input the arguments without an specific order. For example

def func2(a=1,b=3,c=19): return (a-b)/c
func2(b=3,a=13,c=19) == func2(b=3,c=19,a=13)
True

Exercise 1

To get some practice with functions, let us imagine a freely falling particle whose coordinate position (x, y) is described by the relations x(t)=0.5tx(t) = 0.5 t y(t)=9.81t2+0.23t+100.34y(t) = -9.81 t^2 + 0.23 t + 100.34 Write two functions: the first takes t as an argument and returns x(t)x(t), and the second takes t as an argument and returns y(t)y(t). Being a good programmer means also to verify the functionality of your code, so run some tests.

# Write your solution in this cell

Functions have additional features like optional parameters, default parameters, typing, etc., some of them are self explanatory, some of them are not. We encourage you to learn more about functions here.

Lists and Tuples

Lists and tuples are data structures, which as the name explicitly suggests, they are utilized to collect data in one single variable. Let's define a List, to make it more clear.

List

To define a list, we use the following syntax

my_list = [3, 4, 8, 2, 5, 3, 7]

Lists can be slices and indexed similarly to strings. For example:

  • Access a list element by indexing

my_list[2]

returns 8. (Recall Python starts counting from zero, so the the first element is indexed by 0, the second element by 1, etc.) You can also index the last (second to last, etc.) element by using the index -1 (-2, etc.):

my_list[-1]

returns 7.

  • Slice the list by defining the range of interest in the list

slice = my_list[1:4]

this will give as output [4, 8, 2]. Note the last index of the slice is not included, so my_list[1:4] does not include my_list[4].

  • Lists are mutable objects, this means that we can change one or more elements of the list

my_list[3] = "string"

Now, we would have added a string as the fourth element. This is just to show that the elements of a list do not have to be of the same type.

  • Append to a list by using

my_list.append(10)

or you can simply do

my_list += [10]
  • Concatenate two lists

my_list2 = [1, 4, 'r', (3,4)] my_list3 = [5.6, 4, 'c', [1,2,3]] my_list4 = my_list2 + my_list3
  • Determine the length of your list

len(my_list4)

producing as output 8.

Exercise 2

Go ahead and try all these things out!

# Here you have space to play with lists.

Tuples

Tuples are in general similar objects as Lists, but they are defined with round brackets instead of squared ones.

my_list = [3, 4, 8, 2, 5, 3, 7] # This is a list my_tuple = (3, 4, 8, 2, 5, 3, 7) # This is a tuple

Accessing different elements of a tuple can be done in the same way than for lists. However, tuples are not mutable which means that one cannot change or add more elements to the tuple.

Exercise 3

Go ahead and try to use the operations for list in tuples and convince yourself of their main differences

# Here you have space to play with tuples

Lists and tuples can be change one into another, and as usual, python has already default functions for doing that, for instance to go from a list into a tuple we use the function tuple(my_list) , and viceversa, list(my_tuple) .

Convince yourself of this with the following two objects

my_list = [3, 4, 8, 2, 5, 3, 7] # This is a list my_tuple = (3, 4, 8, 2, 5, 3, 7) # This is a tuple

Let us finally point out some useful differences between lists and tuples in the following image, they will be useful on your future programing

from IPython.display import Image Image("Figures/image_1.png")
Image in a Jupyter notebook

Numpy

Here we introduce the numpy library, which is a collection of functions that are fast, efficient, and very useful for scientific computing. To use numpy, first we need to import it.

import numpy as np

Typically, you put your import statements at the very top of your code (in the first code cell) because you need to import a package or module before you can use it in the rest of your code.

You may have noticed that the import statement ends with as np. This is called aliasing. This gives numpy the nickname np, so that every time we call the library we write np instead of numpy. For instance, to call numpy functions we will always use np.function() instead of numpy.function() which is just more convenient but doesn't change the nature of the execution.

As an example, let us use numpy's sqrt() function.

import numpy as np x = 2 y = np.sqrt(x) print(y)
1.4142135623730951

Additional numpy functions include

  1. exponential function: np.exp(x)

  2. logarithmic function: np.log(x)

  3. trigonometric function: np.sin(x), np.cos(x), np.tan(x)

Constants like π\pi and the Euler number ee are also accessible by using the syntax np.pi and np.e respectively.

A full documentation can be seen here.

Exercise 4

Write a function which takes as arguments the coordinates x and y and returns the polar coordinates r and theta. Recall the change of variables from Euclidean to polar coordinates is r=x2+y2θ=arctan(y/x). r = \sqrt{x^2+y^2}\\ \theta = \arctan({y/x}).

# write your function here

Numpy arrays

Numpy arrays are used for storing multiple elements, similar to a list or tuple, and you can think of them as vectors or matrices of numbers. Numpy arrays have a lot of built-in functionality that makes them very useful for scientific computing, and they work hand-in-hand with other libraries like matplotlib (used for making plots and figures) and scipy (used for scientific calculations). Compared to lists, numpy arrays are also fast for evaluation and simplify the code you have to write (see here).

To convert a list into a numpy array we use the following syntax:

a_numpy_array = np.array([2, 3, 4])

Exercise 5

Convert the previous generated list into numpy arrays and calculate the sum of it using this function.

In addition, convince yourself that the indexing, slicing, computing the length of the array, etc., works in a similar way that for lists. However, if you want to add append an element to an array, you do not use the command a_numpy_array.append([value]). Instead, you do np.append(a_numpy_array, value). Add the element 10 to the array a_numpy_array above.

Note: changing the size of an numpy array is generally not recommended, since this is a very expensive operation. If you often need to append or remove elements, consider using a list instead.

# here goes your result.

Next, let us create a new numpy array named t, that looks like this

array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. , 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2. , 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 3. , 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 4. , 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 5. , 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8, 5.9, 6. , 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8, 6.9, 7. , 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7, 7.8, 7.9, 8. , 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9, 9. , 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 9.9, 10.0])

We have two options for this task:

  1. Use np.linspace to create a numpy array from start to stop with steps. An example of this is:

t = np.linspace(0, 10, 101)
  1. Use np.arange to create a numpy array with a fixed step size. An example of this is:

t = np.arange(0, 10.1, 0.1)
# write them explicitly here

Exercise 6

Recall Exercise 1? We created the functions x(t), y(t). In this exercise we want to compute the values for the coordinates positions at every single elements of the array t above, we can simply insert the array into the function. The function will be applied element wise onto that array. For example, your functions on exercise 1 have the structure:

def x(t): #.... x_t = x(t)

the resulting evaluation x_t will be

array([ 1.003400e+02, 1.002649e+02, 9.999360e+01, 9.952610e+01, 9.886240e+01, 9.800250e+01, 9.694640e+01, 9.569410e+01, 9.424560e+01, 9.260090e+01, 9.076000e+01, 8.872290e+01, 8.648960e+01, 8.406010e+01, 8.143440e+01, 7.861250e+01, 7.559440e+01, 7.238010e+01, 6.896960e+01, 6.536290e+01, 6.156000e+01, 5.756090e+01, 5.336560e+01, 4.897410e+01, 4.438640e+01, 3.960250e+01, 3.462240e+01, 2.944610e+01, 2.407360e+01, 1.850490e+01, 1.274000e+01, 6.778900e+00, 6.216000e-01, -5.731900e+00, -1.228160e+01, -1.902750e+01, -2.596960e+01, -3.310790e+01, -4.044240e+01, -4.797310e+01, -5.570000e+01, -6.362310e+01, -7.174240e+01, -8.005790e+01, -8.856960e+01, -9.727750e+01, -1.061816e+02, -1.152819e+02, -1.245784e+02, -1.340711e+02, -1.437600e+02, -1.536451e+02, -1.637264e+02, -1.740039e+02, -1.844776e+02, -1.951475e+02, -2.060136e+02, -2.170759e+02, -2.283344e+02, -2.397891e+02, -2.514400e+02, -2.632871e+02, -2.753304e+02, -2.875699e+02, -3.000056e+02, -3.126375e+02, -3.254656e+02, -3.384899e+02, -3.517104e+02, -3.651271e+02, -3.787400e+02, -3.925491e+02, -4.065544e+02, -4.207559e+02, -4.351536e+02, -4.497475e+02, -4.645376e+02, -4.795239e+02, -4.947064e+02, -5.100851e+02, -5.256600e+02, -5.414311e+02, -5.573984e+02, -5.735619e+02, -5.899216e+02, -6.064775e+02, -6.232296e+02, -6.401779e+02, -6.573224e+02, -6.746631e+02, -6.922000e+02, -7.099331e+02, -7.278624e+02, -7.459879e+02, -7.643096e+02, -7.828275e+02, -8.015416e+02, -8.204519e+02, -8.395584e+02, -8.588611e+02, -8.783600e+02])

and similar for y(t) . Write a code that evaluates x(t) and y(t) for the t array and store them into the variables x_t, y_t. In addition, try to do the same evaluation but using t as a list rather than a numpy array.

#Write your code here

Numpy also allows you to easily do element-wise operations, if the arrays are of equal length. This feature is called broadcasting.

y = np.linspace(0, 1, 10) x = np.linspace(0, 2, 10) print("adding two arrays of the same size:", x+y) # let's compare this operation with list x_list = list(x) y_list = list(y) print() print("adding two list", x_list+y_list)
adding two arrays of the same size: [0. 0.33333333 0.66666667 1. 1.33333333 1.66666667 2. 2.33333333 2.66666667 3. ] adding two list [0.0, 0.2222222222222222, 0.4444444444444444, 0.6666666666666666, 0.8888888888888888, 1.1111111111111112, 1.3333333333333333, 1.5555555555555554, 1.7777777777777777, 2.0, 0.0, 0.1111111111111111, 0.2222222222222222, 0.3333333333333333, 0.4444444444444444, 0.5555555555555556, 0.6666666666666666, 0.7777777777777777, 0.8888888888888888, 1.0]

As you can see, python list do not add element-wise, instead they just concatenate. So you need to be careful, you can't just replace python lists with numpy arrays.

Exercise 7

Use the function defined in Exercise 4 to convert the arrays x_t and y_t to polar coordinates. Store your results into x_p and y_p.

# convert the coordinates here

Plotting using matplotlib

Here we introduce the matplotlib library which is used to make scientific figures. First, we need to import the library using

import matplotlib.pyplot as plt

Just by looking at the import statement, you already know that every function of this library will be executed with

plt.function()

This is in particular useful if you have a large codebase and you don't want to mix up functions while coding.

Now, back to plotting. You can simply plot two numpy arrays or lists against each other (they must have the same length) by using

plt.plot(x_axis_array, y_axis_array)

In the next codeblock, you see an example of a plot.

import matplotlib.pyplot as plt x = [1, 2, 3, 4, 5] y = [1, 4, 9, 16, 25] plt.plot(x, y)
[<matplotlib.lines.Line2D at 0x170ffe25580>]
Image in a Jupyter notebook

You can see, that the graph is kind of edgy. This is due to the fact, that matplotlib is drawing a straight line between every two points. If you add more points, the line will get smoother. If you want to plot only the points, you can use the scatter plot function as follows

plt.scatter(x,y)

Exercise 8

Convince yourself that this give you a scatter plot.

You can include the points and the line in the same plot by using

plt.plot(x,y) plt.scatter(x,y)
# convincing is done here

Exercise 9

Make also a plot of the polar coordinates using results from Exercise 7. Make two plots:

  • Radial plot: tt on the x-axis, r(t)r(t) on the y-axis

  • Angle plot: tt on the x-axis, θ(t)\theta(t) on the y-axis.

Further information on plotting with matplotlib can be found here.

# Code for making your radial plot here
# Code for making the angle plot here

Example: Making a plot with all the bells and whistles

Let us finish the lecture with the following example. We want to make a log-log plot the function f(x)=1/xf(x) = 1/x against the values of xx from 10710^{-7} to 100100.

First, we want to create a numpy array, which contains the values of xx from 10710^{-7} to 100100. For this, I use np.logspace which is a function that creates a numpy array with logarithmic steps.

import numpy as np x = np.logspace(-7, 3, 1000) # Going from 10^-7 to 10^3

Then, we create a function which returns the value of f(x)=1/xf(x) = 1/x.

def f(x): return 1/x

The last step of the calculation is to calculate the values of f(x)f(x) for every value of xx.

f_x = f(x)

Finally, we plot the values of f(x)f(x) against the values of xx.

import matplotlib.pyplot as plt plt.plot(x, f_x, label=r'$f(x)=1/x$') # LaTeX support for graph labels :) plt.xlabel("x") plt.ylabel("f(x)") plt.legend() plt.show()
Image in a Jupyter notebook

As can be seen, the graph looks horrible. This is due to the fact that we use a linear scale for plotting. A better scale would be a logarithmic scale. Logarithmic plots can be created using the plt.loglog() function

plt.loglog(x, f_x, label=r'$f(x)=x^{-1}$', color="green") # LaTeX support for graph labels :) plt.xlabel("x") plt.ylabel("f(x)") plt.legend() plt.show()
Image in a Jupyter notebook

One could have alternatively used the plt.xscale() function to change the x-axis scales, and similar for the y-axis. We can also make a dashed line too.

plt.plot(x, f_x, label=r'$f(x)=x^{-1}$', color="green", linestyle='--') # LaTeX support for graph labels :) plt.xlabel("x") plt.ylabel("f(x)") plt.xscale('log') plt.yscale('log') plt.legend() plt.show()
Image in a Jupyter notebook

You can plot multiple lines on the same figure.

def g(x): return x**2 g_x = g(x) plt.plot(x, f_x, label=r'$f(x)=x^{-1}$', color="green", linestyle='--') plt.plot(x, g_x, label=r'$g(x)=x^{2}$', color="red", linestyle='-.') plt.xlabel("x") plt.ylabel("f(x) and g(x)") plt.xscale('log') plt.yscale('log') plt.legend() plt.show()
Image in a Jupyter notebook