A document from MCS 275 Spring 2024, instructor Emily Dumas. You can also get the notebook file.

Python tour - MCS 275 Spring 2024 - Emily Dumas

This is not an exhaustive tour, but it quickly exhibits some key aspects of the Python language.

Scripts vs REPL vs notebooks

You can run the python interpreter using the interpreter name, usually python or python3. It waits for commands to run, and prints the result of each one. This is the REPL.

You can run a whole file of Python code using python FILENAME.py or python3 FILENAME.py (depending on the interpreter name).

This document is a notebook, which is more like the REPL. Pieces of code appear in cells, and the value of the last expression in the cell is printed in the output. We'll introduce notebooks properly in a future MCS 275 lecture, so for now you can just treat this as a nice way to present many small programs to you.

Variables, assignment, basic types

In [9]:
# Create a variable by assignment.  No separate declaration needed.
x = 275

# What's the value of x now?
x
Out[9]:
275
In [10]:
# Change the value of x a few times; it can change types at any time
x = 10 # a different integer value
x = 3.25 # a float
x = True # a boolean
x = None # None (the null value)
x = "hello" # a string
# AVOID TYPE CHANGES IN MOST CASES as they make programs harder for humans to understand.
In [3]:
# Some integers
1234
1_000_000 # one million (_ is a separator that is ignored, like a comma)
0x1e # hexadecimal
0b1001 # binary
# only the last value in a cell appears in the output, so you should see 9 = 0b1001 below
Out[3]:
9
In [4]:
# Some floats
0.01
-1.3
3e6 # three million.  "e" is for exponent, means 3 * 10**6
Out[4]:
3000000.0
In [5]:
# The only possible values of a boolean.  Note capitalization
True
False # Output will only show this one; it's the last value in this cell
Out[5]:
False
In [6]:
# The only value of type `nonetype` is None.  It is used as a "null", a signal of the absence of something.
# Displaying a None in the REPL doesn't display anything at all, so this cell has no output
None
In [7]:
# Some strings.  Can use " or ' for single-line strings, or """ or ''' for multi-line strings
"2021"  # This isn't a number!  It is in quotes, so it is a string
"It was the best of times"
'It was the worst of times'
"""It was time for MCS 275 lecture
but I didn't zoom installed on my
laptop."""
Out[7]:
"It was time for MCS 275 lecture\nbut I didn't zoom installed on my\nlaptop."

Arithmetic

I skip this section in class as it's likely familiar; the content is here for reference or review.

In [8]:
# addition
2+2
Out[8]:
4
In [9]:
# multiplication
7*8
Out[9]:
56
In [10]:
# division yields a float, even if the result is an integer
6/2
Out[10]:
3.0
In [2]:
# INTEGER DIVISION, with two slashes/  is division of integers that yields and integer
# The remainder is discarded.
6//2
Out[2]:
3
In [3]:
7//2
Out[3]:
3
In [4]:
8//3
Out[4]:
2
In [7]:
# Percent sign means modulus, or "remainder when dividing by"
500 % 7
Out[7]:
3

So 500 days is some number of weeks plus 3 extra days.

In [5]:
# exponentiation is **, and not ^ which is used for this in some other languages
2**10
Out[5]:
1024

The order of operations follows the mnemonic PEMDAS:

  • Parentheses
  • Exponents
  • Multiplication and division (equal precedence)
  • Addition and subtraction (equal precedence) Operations with the same precedence will be done in left-to-right order.
In [12]:
10 / 2 / 5  # The leftmost division is done first, so this becomes (10/2) / 5 which is 1
Out[12]:
1.0
In [13]:
# modulus (remainder upon division) is %
1271845 % 7  # This number is one more than a multiple of 7
Out[13]:
1

Operations on strings

In [14]:
# Join two strings
"hello" + " world"
Out[14]:
'hello world'
In [15]:
# Repeat a string many times using multiplication
"n" + ( "o"*30 )
Out[15]:
'noooooooooooooooooooooooooooooo'

Boolean expressions and logic

In [16]:
# Compare numbers with <, >, =, <=, >=
1 < 5
Out[16]:
True
In [17]:
27 >= 27
Out[17]:
True
In [18]:
27 > 27
Out[18]:
False
In [19]:
# Equality is tested with ==
8 == 9
Out[19]:
False
In [20]:
# Strings compare by dictionary order, with < meaning earlier in dictionary
"aardvark" < "abalone"
Out[20]:
True
In [21]:
"skunk" < "shark"
Out[21]:
False
In [22]:
# Capital letters come before lower case letters in this dictionary
"Zebra" < "airplane"
Out[22]:
True

Numeric conversions

In [23]:
# Convert a string to an integer
int("1500")
Out[23]:
1500
In [24]:
# Convert string to integer, using a base other than 10
int("1001",2) # base 2 = binary, so we get 0b1001 = 9
Out[24]:
9

String conversions

In [25]:
# Convert anything to a string with str()
# Not often needed.  But for example you could use this to 
# get the digits of an integer, since a string lets you
# access individual characters
str(239.1)
Out[25]:
'239.1'
In [26]:
str(False)
Out[26]:
'False'
In [27]:
str([2,7,5])  # Lists are formatted with spaces after the commas
Out[27]:
'[2, 7, 5]'

Useful features of strings

In [28]:
s = "Hello"
# Convert to upper case (returns the converted value, doesn't change s)
s.upper()
Out[28]:
'HELLO'
In [29]:
# Convert to lower case
s.lower()
Out[29]:
'hello'
In [30]:
# Test for prefix
s.startswith("He")
Out[30]:
True
In [31]:
# Test for suffix
s.endswith("llo")
Out[31]:
True
In [32]:
# "string in string" tests for the presence of a substring
"car" in "racecar"
Out[32]:
True
In [33]:
"aaa" in "banana"
Out[33]:
False
In [34]:
# String formatting: .format(value, value, ...) formats the values and puts them into the string where
# there are placeholders

# Basic usage
"I wish I had a pet {} so they could eat {} {}".format("mouse",16,"seeds")
Out[34]:
'I wish I had a pet mouse so they could eat 16 seeds'
In [35]:
# Placeholders can have numbers to indicate which of the arguments they should take
# They are 0-based indices of the list of arguments to .format(...)
"First you put on your {1}, and THEN you put on your {0}".format("shoes","socks")
Out[35]:
'First you put on your socks, and THEN you put on your shoes'
In [36]:
# Floating point placeholders can have a formatting directive in the format :X.Yf
# where X,Y are integers. The number will be formatted to have a width of at least X characters,
# Y of them after the decimal point.  X is optional.
"I am {:3.2f} percent sure of it".format(75.12345)
Out[36]:
'I am 75.12 percent sure of it'
In [37]:
"Rounding to tenths we find that pi is approximately {:.1f}".format(3.14159265359)
Out[37]:
'Rounding to tenths we find that pi is approximately 3.1'

Printing stuff

In a script, you won't see any output if your program just contains expressions, because in script mode, Python doesn't print the results of evaluating each line. You need to explicitly request output.

In [38]:
# Display a message in the terminal
print("Hello world")
# Note: This statement doesn't return a value!
Hello world
In [39]:
# Can print multiple values.  They'll be separated by spaces, by default
print("What if I told you that MCS",275,"was fun?")
# print() can handle every built-in type
print("Here is a list:",[5,4,3,1])
What if I told you that MCS 275 was fun?
Here is a list: [5, 4, 3, 1]
In [40]:
# print() puts a newline at the end by default
# This can be disabled
print("ba",end="")   # no newline after this one
print("nana")
banana

Lists and dicts

In [13]:
L = [2, True, "hello", 0.001]  # a list, note heterogeneous types
D = { "ddumas": "AE1953D8C00F",
      "bkarloff": "41028F0DE021"} # a dict, mapping keys to values
In [14]:
L[2]
Out[14]:
'hello'
In [15]:
D["ddumas"]
Out[15]:
'AE1953D8C00F'

List indexing

Lists are mutable ordered collections of elements accessible by integer index. They are similar to arrays in other languages.

In [42]:
# Elements of a list can have different types.
# Let's set up an example to work with in the next few cells.
L = [ 1, "fish", 2, "fish", False, True, False, None, 5.25, "last"]
In [43]:
# Retrieve an element of a list using brackets with 0-based index.
L[0]  # first element
Out[43]:
1
In [44]:
L[3]  # fourth element
Out[44]:
'fish'
In [45]:
# Negative index means count from the end of the list, with -1 meaning last element
L[-1]
Out[45]:
'last'
In [46]:
# len(listname) gives the length of the list `listname`
len(L)
Out[46]:
10

List slices

In [47]:
# Let's set up a sample list to work with (same one used in a previous section)
L = [ 1, "fish", 2, "fish", False, True, False, None, 5.25, "last"]
In [48]:
# Select a contiguous sublist using [start_idx:end_idx]
# Includes the element at start_idx, but not end_idx
# This is an example of a "slice"
L[2:5]  # Has 5-2 = 3 elements
Out[48]:
[2, 'fish', False]
In [49]:
# Slices that extend past the end of the list don't produce an error
L[5:100000]  # Elements 5 to 99999 requested, but we get 5 to 9
Out[49]:
[True, False, None, 5.25, 'last']
In [50]:
# Slices start at the beginning if the start index is missing
L[:3] # first three elements
Out[50]:
[1, 'fish', 2]
In [51]:
# Slices end at the end if the end index is missing
L[3:] # everything but the first three elements
Out[51]:
['fish', False, True, False, None, 5.25, 'last']
In [52]:
# You can specify that a slice should use a step size other than 1
L[::2]  # Every other element, starting at index 0
Out[52]:
[1, 2, False, False, 5.25]
In [53]:
L[1::2]  # Every other element, starting at index 1
Out[53]:
['fish', 'fish', True, None, 'last']

List conversion and membership testing

In [54]:
# Convert a string to a list to get a list of its characters
list("abcdef")
Out[54]:
['a', 'b', 'c', 'd', 'e', 'f']
In [55]:
# Let's set up a sample list to work with (same one used in a previous section)
L = [ 1, "fish", 2, "fish", False, True, False, None, 5.25, "last"]
In [56]:
# Checking whether an item is an element of a list
5.25 in L
Out[56]:
True
In [57]:
2 in L
Out[57]:
True
In [58]:
"ball" in L
Out[58]:
False

Other useful list features

In [59]:
# set up a list to work with
courses_taken = [ 260, 275 ]
# Add an element at the end with append
courses_taken.append(320) # doesn't return anything, it just modifies the list
In [60]:
# What's in the list now?
courses_taken
Out[60]:
[260, 275, 320]
In [61]:
# An element of a list can be replaced using item assignment
courses_taken[0] = 261  # change first element to 261
In [62]:
courses_taken # Now, the first element should be 261
Out[62]:
[261, 275, 320]
In [63]:
# But you can't add new elements to the list with item assignment
# Only existing elements can be modified
courses_taken[3] = 501  # no item 3 right now, so this gives an error.
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-63-7f114ccb9050> in <module>
      1 # But you can't add new elements to the list with item assignment
      2 # Only existing elements can be modified
----> 3 courses_taken[3] = 501  # no item 3 right now, so this gives an error.

IndexError: list assignment index out of range
In [64]:
# .pop() will remove and return the last element of a list
courses_taken.pop()
Out[64]:
320
In [65]:
# Notice that 320 is no longer in the list
courses_taken
Out[65]:
[261, 275]
In [66]:
# .index(elt) will find elt and return its position
# or raise an exception if it is not found
courses_taken.index(275)  # should return 1 since courses_taken[1] is 275
Out[66]:
1
In [67]:
# .insert(where,what) adds an element at a specific position
# Afterward, listname[where] will be equal to `what`
# Existing items in the list remain, but move to higher indices
# to make room (if needed).
courses_taken.insert(1,401) # Put 401 at index 1, i.e. make it the second element.
courses_taken # Show the resulting list
Out[67]:
[261, 401, 275]
In [68]:
# Sort a list in place
courses_taken.sort() # note lack of any return value
In [69]:
# But after the previous command, the list is now sorted
courses_taken
Out[69]:
[261, 275, 401]

String indexing

In [70]:
# Strings also support integer indices and slices to get characters or substrings
"banana"[0]
Out[70]:
'b'
In [71]:
"banana"[2:4]
Out[71]:
'na'
In [72]:
"banana"[1::2]  # every other letter 
Out[72]:
'aaa'
In [73]:
# Reverse a string
"banana"[::-1]
Out[73]:
'ananab'
In [74]:
# Similar to lists, you can get the total number of characters in a string with len(...)
len("If you have a garden and a library, you have everything you need.")
Out[74]:
65

Dictionaries

Map keys to values. Keys can be of various types (but some restrictions, e.g. lists cannot be keys). Values can be arbitrary types. Written with {} and : to separate key from value.

In [75]:
# Create a dictionary and bind the name "d" to it
d = { "department": "MSCS", "building": 631, "phd_granting": True}
In [76]:
# Access the value associated with a key
d["department"]
Out[76]:
'MSCS'
In [77]:
# Add a new key-value pair to the dictionary
d["college"] = "LAS"
In [78]:
# Test if a **key** is present
"building" in d # True because "building" is a key of this dict
Out[78]:
True
In [79]:
# You can't use `in` to directly check if a value is present
631 in d  # False because 631 is not a key of this dict
Out[79]:
False

Conditionals: if-else-elif

In [80]:
if 5 > 7:
    print("5 is larger")
else:
    print("7 is larger")
7 is larger
In [81]:
x = 15 # adjust this number and run the code to test other branches
if x % 6 == 0: 
    print(x,"is divisible by 6")
elif x % 3 == 0:
    print(x,"is divisible by 3, but is NOT divisible by 6")
else:
    print(x,"is not divisible by 3 (and so not by 6 either)")
15 is divixible by 3, but is NOT divisible by 6

The following is a classic example of the same phenomenon in the previous example: When you consider several conditions, and one is implied by another one, you need to check the more specific one first.

This code will print integers 0 to 99, following each integer with "fizz" if the integer is a multiple of 3, "buzz" if it is a multiple of 5, and "fizzbuzz" if it's a multiple of both.

You must check for being a multiple of both first!

In [20]:
for i in range(100):
    if i%15==0:
        print(i,"fizzbuzz")
    elif i%3==0:
        print(i,"fizz")
    elif i%5==0:
        print(i,"buzz")
    else:
        print(i)
0 fizzbuzz
1
2
3 fizz
4
5 buzz
6 fizz
7
8
9 fizz
10 buzz
11
12 fizz
13
14
15 fizzbuzz
16
17
18 fizz
19
20 buzz
21 fizz
22
23
24 fizz
25 buzz
26
27 fizz
28
29
30 fizzbuzz
31
32
33 fizz
34
35 buzz
36 fizz
37
38
39 fizz
40 buzz
41
42 fizz
43
44
45 fizzbuzz
46
47
48 fizz
49
50 buzz
51 fizz
52
53
54 fizz
55 buzz
56
57 fizz
58
59
60 fizzbuzz
61
62
63 fizz
64
65 buzz
66 fizz
67
68
69 fizz
70 buzz
71
72 fizz
73
74
75 fizzbuzz
76
77
78 fizz
79
80 buzz
81 fizz
82
83
84 fizz
85 buzz
86
87 fizz
88
89
90 fizzbuzz
91
92
93 fizz
94
95 buzz
96 fizz
97
98
99 fizz
In [21]:
s = "carpeting"  # change this word and run the code to test conditional
if "pet" in s:
    print(s,"contains pet as a substring")
    print("In fact, it appears at index",s.find("pet"))
carpeting contains pet as a substring
In fact, it appears at index 3

Loops

In [83]:
# Find a power of 2 that has 7 as a digit
n = 1
while "7" not in str(2**n):
    n = n + 1
# The next line is not indented, so it only runs when the loop is finished
print("Found it: The number 2**{} = {} has 7 as a digit".format(n,2**n))
Found it: The number 2**15 = 32768 has 7 as a digit
In [84]:
# Take each element of a list and print it
available_drinks = ["coffee","tea","juice"]
for drink in available_drinks:
    print("Available drink:",drink)
Available drink: coffee
Available drink: tea
Available drink: juice
In [85]:
# Take each key of a dictionary and do something with it
dept_data = { "department": "MSCS", "building": 631, "phd_granting": True}
for field in dept_data:
    print("{} = {}".format(field,dept_data[field]))
department = MSCS
building = 631
phd_granting = True
In [86]:
# Dictionaries support retrieving key,value pairs
# which can be unpacked into two variables in a loop
for field,value in dept_data.items():
    print("{} = {}".format(field,value))
department = MSCS
building = 631
phd_granting = True
In [87]:
# Break out of a loop early with `break`
L = [ 1, 2, 4, 8, 16, 32, 64, 128, 256, 512 ]
for x in L:
    if "5" in str(x):
        # if we hit an integer containing digit 5, we stop the loop
        break
    print(x)
print("The loop ended, so the next element of L must have contained the digit 5")
1
2
4
8
16
32
64
128
The loop ended, so the next element of L must have contained the digit 5

A slightly more involved example: A little program that repeatly rolls dice or flips coins, until you want to quit.

In [19]:
import random

print("Randomizer Pro v1.0")

while True:
    s = input("Flip (C)oin, Roll (D)ie, or (Q)uit? ")
    s = s.lower()
    
    # Check for invalid input
    if s not in "cdq":
        print("I don't know that command!")
        continue # jump back to the top of this loop
        # Note: continue is often used like this to handle error conditions at the top of a loop
        
    # Check the various valid input possibilities
    if s == "c":
        print(random.choice(["Heads","Tails"]))
    elif s == "d":
        print(random.randint(1,6))
    else:
        # s == "q", so we exit
        break
print("Thanks for using Randomizer Pro v1.0")
Randomizer Pro v1.0
Flip (C)oin, Roll (D)ie, or (Q)uit? C
Tails
Flip (C)oin, Roll (D)ie, or (Q)uit? D
1
Flip (C)oin, Roll (D)ie, or (Q)uit? d
6
Flip (C)oin, Roll (D)ie, or (Q)uit? D
3
Flip (C)oin, Roll (D)ie, or (Q)uit? d
1
Flip (C)oin, Roll (D)ie, or (Q)uit? c
Heads
Flip (C)oin, Roll (D)ie, or (Q)uit? c
Heads
Flip (C)oin, Roll (D)ie, or (Q)uit? c
Tails
Flip (C)oin, Roll (D)ie, or (Q)uit? q
Thanks for using Randomizer Pro v1.0

Files

Be careful: It can be dangerous to run file-related commands, because opening a file for writing will delete any existing file with that name. It is important to make sure that any time you open a file for writing, the filename does not refer to an existing file whose contents you want to keep!

In [ ]:
# Write some text to a file, METHOD 1 (all at once)

# Open a new file (delete if it exists already) for writing
output_file = open("output.txt","wt") # wt = write, text (not binary) file
# Write some text to it.  Note need to add our own newlines.
output_file.write("Hello.\nThis is a text file.\nThat is all.\n")
# Close the file (writes may not actually happen until now)
output_file.close()

Or we could do the same thing but write one line at a time. You can call .write as many times as you like.

In [88]:
# Write some text to a file, METHOD 2 (line by line)

# Open a new file (delete if it exists already) for writing
output_file = open("output.txt","wt") # wt = write, text (not binary) file
# Write some text to it.  Note need to add our own newlines.
output_file.write("Hello.\n")
output_file.write("This is a text file.\n")
output_file.write("That is all.\n")
# Close the file (writes may not actually happen until now)
output_file.close()

Let's read those lines back. First, read all at once:

In [8]:
# Open the file created above, for reading, METHOD 1 (all at once)
input_file = open("output.txt","rt")
# Get the entire content of the file as a single big string (probably lots of \n in it)
s = input_file.read()
input_file.close()

# Now print what we read from the file
# (end="" means don't add a newline, just print s itself)
print(s,end="")  
Hello.
This is a text file.
That is all.

Note you can't call .read() more than once because there is notion of the "current position" in the file. Once you call .read(), your current position is the end of the file. Calling .read() a second time will return nothing, because there's nothing to read after the end of the file.

Using .read() is only appropriate when you want the entire content of the file at once.

In [15]:
# Reading is a one-time operation.  Trying to read file contents twice
# gives no data the second time.
input_file = open("output.txt","rt")
s1 = input_file.read()  # Read everything, after which we're at the end of the file
s2 = input_file.read()  # Reads nothing, already at the end of the file
input_file.close()

print("The first time I read from the file I got:")
print(s1)
print("-"*70)
print("The second time I read from the file I got:")
print(s2)  # will print nothing, because we read nothing
print("(Expect nothing shown above, since we're already at the end of this file.)")
The first time I read from the file I got:
Hello.
This is a text file.
That is all.

----------------------------------------------------------------------
The second time I read from the file I got:

(Expect nothing shown above, since we're already at the end of this file.)

If you're reading a file full of text, it's more likely you want to process it incrementally, one line at a time. That's also possible, but not using .read(). Instead, there's a method .readline() that returns the next line of text, or an empty string if there's no more.

In [16]:
# Open the file created above, for reading, METHOD 2 (line by line)
input_file = open("output.txt","rt")

while True:
    line = input_file.readline()
    if line=="":
        # We reached the end; exit the loop
        break
    print("I just read one more line from the file.  The contents are:")
    print(line,end="")  # line will already have a newline, most likely
    print("This line of the file has {} characters\n".format(len(line)))

input_file.close()
I just read one more line from the file.  The contents are:
Hello.
This line of the file has 7 characters

I just read one more line from the file.  The contents are:
This is a text file.
This line of the file has 21 characters

I just read one more line from the file.  The contents are:
That is all.
This line of the file has 13 characters

Or, instead of calling .readline() repeatedly, you can also just use the file object in a for loop. That will automatically do something for each line. The following is thus a simpler but equivalent form of method 2:

In [17]:
# Open the file created above, for reading, METHOD 2' (line by line)
input_file = open("output.txt","rt")

for line in input_file:
    print("I just read one more line from the file.  The contents are:")
    print(line,end="")  # line will already have a newline, most likely
    print("This line of the file has {} characters\n".format(len(line)))

input_file.close()
I just read one more line from the file.  The contents are:
Hello.
This line of the file has 7 characters

I just read one more line from the file.  The contents are:
This is a text file.
This line of the file has 21 characters

I just read one more line from the file.  The contents are:
That is all.
This line of the file has 13 characters

Comments

In [92]:
# In Python, any text after the symbol # on a line is ignored.  This is a comment.
# There is no multi-line comment syntax in Python; you just put # in front of each.

x = 1  # You can also put comments on the same line as code

# And # it is ok # for comments to contain # any number of # characters.

Use comments to explain code, making it easier for humans to understand.

Usually, a good comment doesn't just say what a line of code is doing, but explains why.

A complete lack of comments is bad. Excessive comments are also bad, especially when they state the obvious. (This review document, which is expository and written for students relatively new to Python, has a number of comments that would be considered excessive/obvious in other contexts.)

In [93]:
# EXAMPLE OF EXCESSIVE COMMENTS - they describe code that is already clear
for i in range(10):   # i from 0 to 9
    x = i*i  # compute the square of i and store in x
    print("Square:",x)  # display the square just computed
Square: 0
Square: 1
Square: 4
Square: 9
Square: 16
Square: 25
Square: 36
Square: 49
Square: 64
Square: 81
In [94]:
# EXAMPLE OF INSUFFICIENT COMMENTS
import math

L = []
for i in range(2,30):
    L.append(i)
    for j in range(2,math.floor(math.sqrt(i))+1):
        if i % j == 0:
            L.pop()
            break
print(L)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
In [95]:
# FIXING THE PREVIOUS EXAMPLE
"""Find the primes up to 29 by the naive (inefficient) method"""
import math # For sqrt

L = [] # to hold the primes
for i in range(2,30):
    L.append(i) # Add i to the list of primes for now; will remove later if composite
    # If i is composite, then one of its divisors is less than or equal to the
    # square root of i, so we need only check up to that point for divisors.
    for j in range(2,math.floor(math.sqrt(i))+1):
        if i % j == 0:
            # Now we know i is composite (divisible by j), so remove it from L and
            # exit the inner loop, moving on to the next prime candidate.
            L.pop()
            break
print(L)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

Docstrings

In Python, the first statement in a file, function body, or class definition should be a string literal on a line by itself. It should contain a description of the file, function, or class. This is called a docstring.

While having a string on a line by itself does nothing in Python, in these cases (first statement of file, function, class), Python will store the string and make it available as part of the built-in help.

Most explanatory text in a Python program should be in comments. Docstrings go in just a few special places, and describe a file, class, or function.

In [96]:
# Suppose this cell contains the full contents of hello.py
# Print a greeting    <--- this describes the file, but it isn't a docstring!  Not a string literal
# "Print a greeting"  <--- also not a docstring.  It's a comment, not a string literal
"""Greet the user"""   # <--- THIS IS A DOCSTRING
"""by saying hello"""  # BAD! Not a docstring, as it isn't the first statement.
                       #      It should be a commend instead

# Comment line in the middle of the file.  That's fine.


print("Hello user!")
Hello user!
In [97]:
# This example involves functions.  See the next section for details about functions.

def f(x):
    """Compute the 'smoothstep' polynomial at x and return."""  # <--- DOCSTRING (even without this comment)
    return 3*x**2 - 2*x**3

print("f({}) = {}".format(0.1,f(0.1)))
f(0.1) = 0.028000000000000004
In [98]:
# Retrieve help about f(...), which includes the docstring.
help(f)
Help on function f in module __main__:

f(x)
    Compute the 'smoothstep' polynomial at x and return.

Functions

Functions provide reusable, named (usually) code blocks that can be called (run) at any point in a program, can accept data from the program, and can return data.

In [99]:
# Basic example

def clamp(x):
    """Clamp x between 0 and 1, i.e. if x is less than 0, return 0.
       If x is greater than 1, return 1.  Otherwise return x."""
    if x<0:
        return 0.0
    if x>1:
        return 1.0
    return x

for i in range(17):      # iterate over 0...16
    t = (i / 10) - 0.3   # so t ranges from -0.3 to 1.3, with steps of size 0.1
    print("clamp({:.2f}) = {:.2f}".format(t,clamp(t)))
clamp(-0.30) = 0.00
clamp(-0.20) = 0.00
clamp(-0.10) = 0.00
clamp(0.00) = 0.00
clamp(0.10) = 0.10
clamp(0.20) = 0.20
clamp(0.30) = 0.30
clamp(0.40) = 0.40
clamp(0.50) = 0.50
clamp(0.60) = 0.60
clamp(0.70) = 0.70
clamp(0.80) = 0.80
clamp(0.90) = 0.90
clamp(1.00) = 1.00
clamp(1.10) = 1.00
clamp(1.20) = 1.00
clamp(1.30) = 1.00
In [100]:
# Functions can take multiple arguments, which are required if no default value is given
def clamp2(x,minval,maxval):
    """Clamp x between minval and maxval"""
    if x<minval:
        return minval
    if x>maxval:
        return maxval
    return x

for i in range(11):   # iterate over 0...10
    t = (i / 10)     # so t ranges from 0 to 1, with steps of size 0.1
    print("clamp2({:.2f},0.2,0.8) = {:.2f}".format(t,clamp2(t,0.2,0.8)))
clamp2(0.00,0.2,0.8) = 0.20
clamp2(0.10,0.2,0.8) = 0.20
clamp2(0.20,0.2,0.8) = 0.20
clamp2(0.30,0.2,0.8) = 0.30
clamp2(0.40,0.2,0.8) = 0.40
clamp2(0.50,0.2,0.8) = 0.50
clamp2(0.60,0.2,0.8) = 0.60
clamp2(0.70,0.2,0.8) = 0.70
clamp2(0.80,0.2,0.8) = 0.80
clamp2(0.90,0.2,0.8) = 0.80
clamp2(1.00,0.2,0.8) = 0.80
In [101]:
# Default values can be specified, and if they are, the argument is optional.
def clamp3(x,minval=0.0,maxval=1.0):
    """Clamp x between minval (default 0) and maxval (default 1)"""
    if x<minval:
        return minval
    if x>maxval:
        return maxval
    return x

print("clamp3({:.2f}) = {:.2f}".format(t,clamp3(-0.2)))  # minval, maxval get default values
print("clamp3({:.2f},-1,1) = {:.2f}".format(t,clamp3(-0.2,-1,1)))  # minval, maxval specified
clamp3(1.00) = 0.00
clamp3(1.00,-1,1) = -0.20
In [102]:
# When calling a function, you can specify arguments by name using name=value syntax
# These *keyword arguments* or *kwargs* can appear in any order, whereas arguments
# given as values alone must appear in the same order as the definition.

print(clamp3(minval=0.4,x=0.3))  # Using kwargs to choose the argument order

# kwargs are really useful for functions with many arguments, most of which have default
# values.  Often you want to call them and change just a few arguments from defaults.
0.4
In [103]:
# Functions don't need to return anything.  If the `return` keyword is not used,
# the function returns None

def hello():
    """Print a friendly greeting"""
    print("Hello there!")

hello() == None  # True, because no return means return None
Hello there!
Out[103]:
True

Modules

Modules allow you to put a bunch of code in a separate file so it can be reused and is contained (allowing it to be used without the user worrying about all the details). Splitting code for a large program between multiple files also makes it easier to browse the source and to maintain the program.

In [104]:
#%%writefile polynomials.py
# Example module
# To actually use this, either:
#     1) Save the entire cell contents in a file "polynomials.py"
# or  2) Remove the # in front of the first line of this cell and run in a notebook
"""Some useful polynomials and other functions"""

USEFUL_CONST = 1234.0   # all caps is *not* required

def nozeros(x):
    """A quadratic polynomial with no real zeros"""
    return x**2 + 1

def smoothstep(x):
    """Smoothstep polynomial (clamped between 0 and 1)"""
    if x<0:
        return 0.0
    if x>1:
        return 1.0
    return 3*x**2 - 2*x**3
In [105]:
"""Example of using the polynomials module"""
# (Won't work unless you've saved the cell above to polynomials.py in the same directory where
#  you are running this code.)

import polynomials

print(polynomials.USEFUL_CONST)
print(polynomials.nozeros(0.2)) # should be 1.004
print(polynomials.smoothstep(0.45)) # should be 0.42525
1234.0
1.04
0.42525
In [106]:
# There are many useful modules in the Python standard library
# A few examples follow

import sys
print(sys.version) # Python version
print(sys.argv[0]) # Name of the currently running script
3.8.5 (default, Jul 28 2020, 12:59:40) 
[GCC 9.3.0]
/usr/lib/python3/dist-packages/ipykernel_launcher.py
In [107]:
import time

print("Working...")
time.sleep(1.25) # Wait for 3.1 seconds
print("Done!")

print("It is now {} seconds since 0:00 on Jan 1 1970 GMT".format(time.time()))
Working...
Done!
It is now 1610652081.4329822 seconds since 0:00 on Jan 1 1970 GMT
In [111]:
import os

print("Files in the current directory:")
for fn in os.listdir("."):
    print(fn)

# Check for a file that exists (for the instructor developing this worksheet)
if os.path.exists("output.txt"):
    print("A file with name '{}' exists in the current directory.".format("output.txt"))

# Check for a file that does not exist
if os.path.exists("worksheet1soln.ipynb"):
    print("A file with name '{}' exists in the current directory.".format("worksheet1soln.ipynb"))
Files in the current directory:
polynomials.py
python_tour.html
__pycache__
.ipynb_checkpoints
output.txt
python_tour.ipynb
mk_contents.py
A file with name 'output.txt' exists in the current directory.

Classes

In Python, classes provide a way to define custom types that combine attributes (data) and methods (behavior).

They are the basis of object-oriented programming in Python.

Classes can extend other classes, allowing customization of certain properties while reverting to the parent class behavior on others.

In [109]:
# Basic 2D point class
import math # for sqrt

class Point:
    """A point in the xy-plane"""  # DOCSTRING
    def __init__(self,x,y):
        """Initialize new point instance"""
        self.x = x
        self.y = y
    def translate(self,dx,dy):
        """Move the point by a vector (dx,dy)"""
        self.x += dx
        self.y += dy
    def distance_to(self,other):
        """Distance from this point to another one"""
        deltax = other.x - self.x
        deltay = other.y - self.y
        return math.sqrt(deltax**2 + deltay**2)

P = Point(3,4)  # Creates instance of Point, which calls __init__(...)
print("Distance from (3,4) to the origin: {:.2f}".format(P.distance_to(Point(0,0))))
print("Before moving: P.x={}, P.y={}".format(P.x,P.y))
print("Moving by (5,1)")
P.translate(5,1)  # Modifies P, returns nothing!
print("After moving:  P.x={}, P.y={}".format(P.x,P.y))
Distance from (3,4) to the origin: 5.00
Before moving: P.x=3, P.y=4
Moving by (5,1)
After moving:  P.x=8, P.y=5
In [110]:
# Class help includes docstrings of the class itself and its methods
help(Point)
Help on class Point in module __main__:

class Point(builtins.object)
 |  Point(x, y)
 |  
 |  A point in the xy-plane
 |  
 |  Methods defined here:
 |  
 |  __init__(self, x, y)
 |      Initialize new point instance
 |  
 |  distance_to(self, other)
 |      Distance from this point to another one
 |  
 |  translate(self, dx, dy)
 |      Move the point by a vector (dx,dy)
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)