Python 102
The reading is fairly long. It covers Python skills that you may already have, and so hopefully that will be easier to read. We will use all of these skills in our PyMySQL/Flask apps, but these are all generic Python skills.
Python Skills¶
All of you said you know Python, though some of you have said
privately that it's been a while, and some parts of it have faded a
bit since CS 111 or whenever you last used it. Furthermore, CS 111
doesn't teach the whole language. Here are a few topics that students
have wanted more information on. So, if CS 111 (or its equivalent) is
Python 101
, this is Python 102
.
Let me know if there are other aspects of Python you'd like to learn more about.
- keyword arguments
- formatting strings
- Modules
- Import
- Dictionaries
- Dates and Times
- Exceptions
Keyword Arguments¶
Most functions will use positional arguments:
def reciprocal(x):
return 1/x
def ratio(x,y):
return x/y
This works well, is compact and simple, and is our basic technique.
It doesn't scale well, though. What if a function takes a dozen arguments and most of them are optional, with default values? A cool feature that Python has (and so do many other languages, but not all) is keyword arguments.
For this example, we have to assume that we are representing a row of a table (think databases) with a Python list. This is for simplicity of the example, not for its realism. The point is the arguments and how we invoke the function, not the function's code.
def make_pet(name, age=None, weight=0.0, kind=None):
return [name, kind, age, weight]
pet1 = make_pet('scabbers', age=8, weight=1.2, kind='rat')
pet2 = make_pet('hedwig', kind='owl', age=2)
pet3 = make_pet('crookshanks', kind='cat')
print(pet1)
print(pet2)
print(pet3)
The make_pet
function has one required, positional argument, and
three optional, keyword arguments.
Copy/paste that code into a Python shell to see what it does! You should get something like:
>>> print(pet1)
['scabbers', 'rat', 8, 1.2]
>>> print(pet2)
['hedwig', 'owl', 2, 0.0]
>>> print(pet3)
['crookshanks', 'cat', None, 0.0]
Notice how Scabbers used all four arguments and in their usual order,
but Hedwig only used two of the three optional arguments, letting the
weight
default to 0.0. Hedwig also swapped the order of the two
keyword arguments that it used. Finally, Crookshanks left out two
keyword arguments, letting them both default.
Keyword arguments allow you to
- omit arguments, in which case they get a default value
- put the keyword arguments in any order
This is very nice! In CS 304, we won't be defining functions with keyword arguments (though you're welcome do if you want), but we will be using many of them. It helps to know about them.
Formatting Strings¶
Python has an interesting history of formatting strings, by which I mean combining fixed text with dynamic data. I'm not going to cover all of it. But here are a few.
Running example: If you have a variable num_students
and you want to
have a string like there are 30 students in CS 304
, but with
the number filled in from the variable's value, you're trying to
combine some static text with a dynamic data. Let's look at a few ways
to do that.
String Concatenation
In Python, you can glue (concatenate) two strings with a +
operator. They must be strings. So, you can do:
num_students = 30
msg = 'There are ' + str(num_students) + ' students in CS 304'
print(msg)
Simple and easy, but mixes variables and strings in a way that many people find hard to read. Not my favorite.
C style
Python has a %
operator that can combine a format string with a
tuple of data:
num_students = 30
msg = 'There are %d students in CS 304' % (num_students,)
print(msg)
This style is considered somewhat obsolete, but you'll see vestiges of it, particularly in prepared queries. Because it's obsolescent, I won't say more now.
The Format Method
This is the technique I advocate:
num_students = 30
msg = 'There are {n} students in CS 304'.format(n=num_students)
print(msg)
This allows you to name the placeholders in your format string, and fill them in with keyword-argument syntax. Very nice.
If the string is long, you can use triple-quoted strings. If you want
to put the .format
on the next line, be sure to wrap the whole right
hand side in parentheses:
msg = ('''
{elven} for the Elven Kings, under the sky
{dwarf} for the Dwarf Lords, in their halls of stone
{mortal} for the Mortal Men, doomed to die,
{sauron} for the Dark Lord on his dark throne...'''
.format(elven='Three',
dwarf='Seven',
mortal='Nine',
sauron='One'))
print(msg)
The .format
method's syntax also bears some resemblance to Jinja2
templating. That's not a coincidence.
There's a variant of the .format
method that is positional:
num_students = 30
msg = 'There are {} students in CS 304'.format(num_students)
print(msg)
By "positional" I mean that the {}
placeholders are replaced with
arguments by their position in the string. So:
msg = 'I like {}, {}, {} and {}'.format('apples','bananas','chocolate','dairy')
print(msg)
The positional form is nice when things are short, but is more awkward when the string gets longer and there are more filler values. Then the keyword style is nicer. Keyword arguments can also be more clear:
msg = 'Dividing {num} by {denom} yields {quo}'.format(denom=4, num=1, quo=0.25)
print(msg)
F-strings
A very new (as of Python 3.6) option for string formatting is f-strings. None of my examples use these (since they pre-date Python 3.6), but you're welcome to use f-strings in your programs if you'd like.
I have a bit more to say about f-strings if you're curious.
Modules¶
Pretty much every civilized programming language has a way to break up big complex programs into separate parts, usually called modules.
A module is a collection of Python functions, classes, variables and such. A package is a collection of modules in a hierarchy. We're not going to worry about the difference, and I'll use the terms pretty interchangeably.
Usually a module is contained in a file, and the name of the file
matches the name of the module. For example, the cs304dbi
module is
in the cs304dbi.py
file.
If you'd like to understand more about the concept, you can read more about the concept of modules
To use the code in a module, adding its functionality to your own program, you import the module. We'll turn to that now.
Import¶
Simple uses of import
are, well, simple. To find out your UID, we
can import the os
module and use the getuid
method defined by it.
import os
myuid = os.getuid()
print(myuid)
The preceding is a complete, working Python script, though it doesn't do much.
Because the os module is one of the standard modules, we don't have to install it in our virtual environment.
Another standard module is the datetime
module. The
datetime modules
defines several classes, including one called datetime
, which
combines the features of the date
class and the time
class.
Here's a simple use of the datetime
module to print the current
time. We saw this code in the servertime.py
example:
import datetime
dt = datetime.datetime.now()
print(dt.strftime('%A %B, %d, %Y at %H:%M %p in %Z'))
That imports the datetime
module, and to refer to the datetime
name in that module, we have to use the dotted notation:
module.name
. Then to use the now
method, we use yet another dot.
It can be convenient sometimes to import the names so that you can
avoid the module.name
syntax. So the example above could be
re-written as:
from datetime import datetime
dt = datetime.now()
print(dt.strftime('%A %B, %d, %Y at %H:%M %p))
both print
Saturday September, 19, 2020 at 13:33 PM
Note: it looks redundant to say from datetime import datetime
but
those are different things that happen to have the same name. The
first datetime
is the name of a module. Inside that module is a
class called datetime
. This is a little confusing, but don't let it
throw you. As you know, a module is a kind of collection or container,
and a class can be in that collection/container.
The strftime
method formats the datetime object. The cryptic string
that is the first argument contains a template with a bunch of
formatting codes. There are a ton of these % format
codes. (This
might remind you of one of Python's string-formatting techniques; no
coincidence.) This particular string shows the day of the week, as as
string in the local language, the name of the month, the date of the
month, the hour and minute with am/pm. There's more about dates and
times in that later section.
From module import¶
Returning to the import
statement, we have:
from module import (name1, name2, ...)
We'll use this technique a lot with Flask, to avoid having to prefix
all of the names with flask.
like this:
from flask import (Flask, render_template, make_response, url_for, request,
redirect, flash, session, send_from_directory, jsonify)
The parentheses are only necessary if, as above, we want to list more names than can fit on one line.
Warning: importing names like this means that they will conflict with
any local identical names. So, we couldn't have a different name in
our app called, for example, request
, because this would be the same
name as the one we are importing from Flask. This is not really a new
idea: you already know that you can't have two different global
variables named fred
. Importing just gives you a new source of
names.
Import *¶
Note that if we want to import every name from another module, we can use a wildcard:
from some_module import *
This is generally considered a bad idea. It completely tears down the wall between two modules. The point of modules is to build walls between different parts of our program, thereby giving more freedom of naming and clarifying where things come from. So tearing down the walls is usually wrong.
However, sometimes it can be useful. See advanced import, but only once you know some Flask.
Importing Local Files¶
We've looked at importing os
and datetime
, both of which are
standard modules, so they don't have to be installed. However, what if
you want to import a file of your own making. In fact, we saw just
that, above: the file all_examples.py
imported names from
example_form_processing_one_route.py
.
For that to work, both files must be in the same directory. Say
app.py
imports servertime
. If you copy app.py
to another
directory and run it, you'll get an error like this:
$ python app.py
Traceback (most recent call last):
File "app.py", line 1, in <module>
import servertime
ModuleNotFoundError: No module named 'servertime'
If that happens, consider the following possibilities:
- It's not in your local directory
- You misspelled the name of the module
- It's not in your venv
Dictionaries¶
Dictionaries are very useful and powerful. Every civilized language should have them, and, fortunately, you are living in time when most of the languages you will use will have them. When we do queries of the database, we will typically return our values as dictionaries. (But see the next section about tuples.)
If you've forgotten a lot about dictionaries in python that W3Schools link will refresh your memory.
Terminology note: Dictionaries store key/value pairs. The key is how we look up the value. This is similar to how we look up a row in a database table: we look it up with a key, which uniquely specifies the row we want. So the term key is used in a similar way for both Python dictionaries and SQL tables: it's the way you look something up.
This section reviews a couple of useful methods for Python dictionaries. See online references for more information about all python dictionary methods
Dictionaries can be used as a generalization of arrays:
pets = {}
pets['harry'] = 'hedwig'
pets['ron'] = 'scabbers'
pets['hermione'] = 'crookshanks'
for owner,pet in pets.items():
print('{} owns {}'.format(owner, pet))
The first few lines creates an empty dictionary and then stores values in it using assignment statements similar to those for lists.
The .items()
method returns an iterator with a tuple for each
key/value pair.
(Also notice the use of the positional format
method.)
Running that code yields:
harry owns hedwig
ron owns scabbers
hermione owns crookshanks
We can also read values out using the square bracket notation:
print('Harry owns {}'.format(pets['harry']))
But there's a small pitfall in that. If we mispell the key or
otherwise use a key that's not in the dictionary, we get an error,
specifically a KeyError
:
>>> print('Hermione owns {}'.format(pets['hermoine']))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'hermoine'
>>>
KeyErrors are annoying at best. Sometimes, we aren't sure whether the
value is in the dictionary. An alternative is to use the .get()
method, which has the key (as a string) as the first argument and
takes an optional second argument that is default value. That is,
it's the value to return if the key is not in the dictionary. For
example:
>>> print('Malfoy owns {}'.format(pets.get('malfoy','no pet')))
Malfoy owns no pet
If you omit the second argument, None
is used:
>>> print('Malfoy owns {}'.format(pets.get('malfoy')))
Malfoy owns None
To remove a value from a dictionary, we can use the .pop()
method. So, after The Prisoner of Azkaban, we would do:
pets.pop('ron')
to remove Ron and his pet Scabbers from the pets
dictionary.
The Flask system represents requests and other things as
"dictionary-like" objects, and consequently we will have occasion to
use the .get()
and .pop()
methods from time to time.
In fact, dictionaries and dictionary-like objects are very common in Computer Science. We'll see lots of key/value collections in this course.
Tuples¶
Tuples and lists are pretty easy to work with, but require numeric indices, and that can lead to opaque code. The following is hard to read:
heros = ('harry','ron','hermione','neville','luna','fred','george',
'dumbledore','lupin', 'mad-eye','mcgonagall')
print('My favorite hero is {}'.format(heros[7])) # who?
If the print statement is in a different function from the assignment statement, maybe in a different file, the code becomes downright opaque.
One technique that can help at times is destructuring assignment. Destructuring assignment is a general feature of several languages, including Python and JavaScript. Let's see some examples first:
(x, y, z) = (0, 0, 0) # initialize all to zero
(a, b) = (b, a) # swap two variables
The idea is that there's a tuple of variable names on the left hand side and a tuple of values on the right hand side, and each varible is assigned the corresponding value. So, we could compute the next step of the Fibonacci sequence in one step:
(fib_next, fib, fib_prev) = (fib+fib_prev, fib_next, fib)
In Python, the parentheses on the left hand side are optional:
x, y, z = 0, 0, 0 # initialize all to zero
a, b = b, a # swap two variables
fib_next, fib, fib_prev = fib+fib_prev, fib_next, fib
If we fetch a tuple from the database, we can pull it apart in one easy step:
curs.execute('select min(sales), average(sales), max(sales) from ...')
min, avg, max = curs.fetchone()
Here's another example. Suppose we get four columns in a tuple query
and we don't want to be referring to row[3]
and the like, we can
create four local variables in one quick assignment:
curs = dbi.cursor(conn)
curs.execute('select nm, name, birthdate, addedby from person')
for row in curs.fetchall():
nm, name, bday, staff = row
print('{} ({}) was born on {} and added by {}'.format(name, nm, bday, staff)
The next to last line is an assignment of four variables at once, being the four elements of the tuple. (Lists work the same way.)
Tuple Pitfall
There's one pitfall for tuples that is a little tricky, because they use parentheses. Consider the following:
x = ()
y = (1)
z = (2,3)
This looks like three assignments of tuples, of length 0, 1 and 2, but not so:
- x is a tuple of length 0
- y is an integer
- z is a tuple of length 2
This is weird and inconsistent, but arises because we can also use
parentheses for non-tuple use in arithmetic expressions, so the right
hand side of the assignment to y
looks like that. So, how do you get
a tuple of length 1? You have to put a comma in the parentheses. The
following works as expected:
x = ()
y = (1,)
z = (2,3)
Dates and Times¶
MySQL represents dates and times with the ISO standard of YYYY-MM-DD
HH:MM:SS. Our PyMySQL API automatically parses those and represents
them as Python datetime
objects.
If the user has handed you a date as 'MM/DD/YYYY' and you want to
parse it, the datetime
module provides you the strptime
method to
parse a time string:
from datetime import datetime
string = '02/14/2020' # valentine's day
dt = datetime.strptime(string, '%m/%d/%Y')
print(dt.strftime('%A')) # day of the week
print(dt.strftime('%Y-%m-%d')) # MySQL format
The output is:
Friday
2020-02-14
Exceptions¶
Sometimes things go wrong, which we call run-time (or runtime) errors. There may be other anomalous situations which aren't really errors, but are similar.
One option is to check first (look before you leap
). Here's an example:
def inefficiency(distance, fuel):
if distance == 0:
return 'div zero' # special value for /0
else:
return fuel/distance
Then, of course, the caller has to think about the error as well. On my old gas-powered Subaru Outback, the instantaneous mileage display would have "--" in the two-digit dashboard display when I was idling at a stoplight: infinite inefficiency.
def update_display(distance, fuel):
val = inefficiency(distance, fuel)
if val == 'div zero':
display.update('inefficiency,'--')
else:
display.update('inefficiency,'{:2d}'.format(val))
(The {:2d}
is a code for 2-digit string formatting in Python. See
Python String
formatting if you'd
like to learn more.)
The "check-first" strategy is fine, but we have to return special values. Sometimes a very long way. So, I'm going to switch to a more abstract example.
Suppose function a
calls function b
which calls function c
which
calls function d
which calls function e
. The last function might
get an error. The function a
might be the best function to display
the error message or otherwise handle it.
For example, function e
function discover that a filename was in use
and therefore can't be created. Function a
might be a user-interface
function and therefore has the ability to tell the user that they
chose a filename that is in use and they should pick a different
one. Yuck!
- We don't want
a
to have to do the filename checking thate
does, so we can't check there. Functione
should do so (or get the error) - Function
e
would have to return a special error code, which would have to be passed throughd
,c
andb
to get toa
. That junks up those functions for no good reason.
This style is still used in low-level languages like C
, which has an
errno
code for many operating system calls which then has to be
passed around.
For situations like these, higher-level languages like Python (and Java and JavaScript) use Exceptions:
- The code gets the error or notices the anomaly, like
e
, raises or throws the exception. - The code that handles the error/anomaly, like
a
, catches the exception.
(If you're curious about how this is implemented, you can talk to me in office hours. Briefly, the exception-raising code crawls back through the call stack to find a handler.)
So, the handler does:
def a():
filename = input('enter filename: ')
try:
b(filename, other, args)
except Exception:
print('Sorry, that filename is in use. Please try again')
and the code that discovers the error raises the exception:
def e(filename):
if os.file_exists(filename):
raise Exception
normal code
Ideally, you create or use different exceptions for different kinds of anomalous conditions, which might be handled differently. This example shows some of the built-in exceptions:
def a():
filename = input('enter filename: ')
try:
b(filename, other, args)
f(more, code, here)
except FileExistsError:
print('Sorry, that filename is in use. Please try again')
except KeyError:
print('Sorry, we got a key error. Oops.')
except ZeroDivisionError:
format('Sorry, I divided by zero. My bad.)
Python can allow us to define our own exceptions, but we won't be doing so in this course. Feel free to search the web if you want to learn how.
We typically will not raise exceptions in this course, but PyMySQL and Flask might do so. Sometimes, we will catch these exceptions.
One cool effect of Exceptions is that sometimes you abandon checking beforehand and just plunge ahead, allowing the exception to occur. So, we could re-write some of our code above:
def inefficiency(distance, fuel):
return fuel/distance # might raise an exception
def update_display(distance, fuel):
try:
val = inefficiency(distance, fuel)
display.update('inefficiency,'{:2d}'.format(val))
except ZeroDivisionError:
display.update('inefficiency,'--')
This nicely segregates the code that handles the unusual or anomalous situation from the usual or normal situations.
Conclusion¶
If you discover some aspect of Python that would have been helpful to review as part of CS 304, please let me know.
You can stop reading here, unless you want to know more. If so, you can read the appendices