Session Two: Functions, Booleans and Modules

Review/Questions

Review of Previous Session

  • Values and Types
  • Expressions
  • Intro to functions

Clarifications

  • Python Development Accelerator
  • Blank lines don’t always end a block

Review of Previous Session

  • Values and Types
  • Expressions
  • Intro to functions

Homework Review

Any questions that are nagging?

Today’s Plan

  • Github Upstream
  • Functions
  • Booleans
  • Modules

For Each Section

  • Read through the puzzle for that section.
  • Pick a partner. Describe what your goal is.
  • Read through the section. Try typing any code you see in ipython or python
  • Come up with three questions as you are reading with your partner.
  • We’ll come around and help you.
  • We’ll regroup and you’ll teach me the slides.
  • We’ll solve the puzzle together.

Git Work

Let’s get to know your fellow students!

Git Work Puzzle

We want to pull your classmates “Gitting to Know You” introductions from the (upstream) class repo.

Remember, do these steps:

  • Read through the puzzle for that section.
  • Pick a partner, describe what your goal is.
  • Read through the slides. Try typing any code you see in ipython or python
  • Come up with three questions as you are reading with your partner.
  • We’ll come around and help you.
  • We’ll regroup and you’ll teach me the slides.
  • We’ll solve the puzzle together.

Working with an Upstream

You’ve created a fork of the class repository from the codefellows account on GitHub.

You’ve pushed your own changes to that fork, and then issued pull requests to have that worked merged back to the codefellows original.

You want to keep your fork up-to-date with that original copy as the class goes forward.

To do this, you use the git concept of an upstream repository.

Since git is a distributed versioning system, there is no central repository that serves as the one to rule them all.

Instead, you work with local repositories, and remotes that they are connected to.

Cloned repositories get an origin remote for free:

$ git remote -v
origin  https://github.com/cewing/sea-c34-python.git (fetch)
origin  https://github.com/cewing/sea-c34-python.git (push)

This shows that the local repo on my machine originated from the one in my gitHub account (the one it was cloned from)

You can add remotes at will, to connect your local repository to other copies of it in different remote locations.

This allows you to grab changes made to the repository in these other locations.

For our class, we will add an upstream remote to our local copy that points to the original copy of the material in the codefellows account.

$ git remote add upstream https://github.com/codefellows/sea-c34-python.git

$ git remote -v
origin  https://github.com/cewing/sea-c34-python.git (fetch)
origin  https://github.com/cewing/sea-c34-python.git (push)
upstream  https://github.com/codefellows/sea-c34-python.git (fetch)
upstream  https://github.com/codefellows/sea-c34-python.git (push)

To get the updates from your new remote, you’ll need first to fetch everything:

$ git fetch --all
Fetching origin
Fetching upstream
...

Then you can see the branches you have locally available:

$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/gh-pages
  remotes/origin/master
  remotes/upstream/gh-pages
  remotes/upstream/master

(the gh-pages branch is used to publish these notes)

Finally, you can fetch and then merge changes from the upstream master.

Start by making sure you are on your own master branch:

$ git checkout master

This is really really important. Take the time to ensure you are where you think you are.

Then, fetch the upstream master branch and merge it into your master:

$ git fetch upstream master
From https://github.com/codefellows/sea-c34-python.git
 * branch            master     -> FETCH_HEAD

$ git merge upstream/master
Updating 3239de7..9ddbdbb
Fast-forward
 Examples/README.rst              |  4 ++++
...
 create mode 100644 Examples/README.rst
...

NOTE: you can do that in one step with:

$ git pull upstream master

Now all the changes from upstream are present in your local clone.

In order to preserve them in your fork on GitHub, you’ll have to push:

$ git status
On branch master
Your branch is ahead of 'origin/master' by 10 commits.
  (use "git push" to publish your local commits)
$ git push origin master
Counting objects: 44, done.
...
$

(A simple git push will usually do the right thing)

You can incorporate this into your daily workflow:

$ git checkout master
$ git pull upstream master
$ git push
[do some work]
$ git commit -a
[add a good commit message]
$ git push
[make a pull request]

Git Work Puzzle Solved!

We wanted to pull your classmates “Gitting to Know You” introductions from the (upstream) class repo.

When you make that happen, congratulations! Find your partner’s introduction and read them to each other.

Some Needed Plumbing

Because there’s a few things you just gotta have:

  • collections
  • looping

Collections and Looping

It turns out you can’t really do much at all without at least a collection (container) type, conditionals and looping...

if and elif allow you to make decisions:

if a:
    print(u'a')
elif b:
    print(u'b')
elif c:
    print(u'c')
else:
    print(u'that was unexpected')

What’s the difference between these two:

if a:
    print(u'a')
elif b:
    print(u'b')
## versus...
if a:
    print(u'a')
if b:
    print(u'b')

Try it at http://pythontutor.com

Many languages have a switch construct:

switch (expr) {
  case "Oranges":
    document.write("Oranges are $0.59 a pound.<br>");
    break;
  case "Apples":
    document.write("Apples are $0.32 a pound.<br>");
    break;
  case "Mangoes":
  case "Papayas":
    document.write("Mangoes and papayas are $2.79 a pound.<br>");
    break;
  default:
    document.write("Sorry, we are out of " + expr + ".<br>");
}

Not Python

use if..elif..elif..else

(or a dictionary, or subclassing....)

A way to store a bunch of stuff in order

Pretty much like an “array” or “vector” in other languages

a_list = [2, 3, 5, 9]
a_list_of_strings = [u'this', u'that', u'the', u'other']

Another way to store an ordered list of things

a_tuple = (2, 3, 4, 5)
a_tuple_of_strings = (u'this', u'that', u'the', u'other')

Tuples are not the same as lists.

The exact difference is a topic for next session.

Sometimes called a ‘determinate’ loop

When you need to do something to everything in a sequence

In [10]: a_list = [2, 3, 4, 5]

In [11]: for item in a_list:
   ....:     print(item)
   ....:
2
3
4
5

Try it at http://pythontutor.com

Range builds lists of numbers automatically

Use it when you need to do something a set number of times

In [12]: range(6)
Out[12]: [0, 1, 2, 3, 4, 5]

In [13]: for i in range(6):
   ....:     print(u'*', end=u' ')
   ....:
* * * * * *

Try it at http://pythontutor.com

This is enough to get you started.

Each of these have intricacies special to python

We’ll get to those over the next couple of classes

Functions

Functions Puzzle

In your local repo, after you’ve updated from upstream, go to session02 and find the file stackoverflow.py.

In it, you will find a function that calls itself.

  • What problems does this cause?
  • Why do you think the problem occurs?
  • How can you count the number of times a function can call itself?
  • Modify the program to implement your solution.

Remember, Do These Steps

  • Read through the puzzle for that section.
  • Pick a partner. Describe what your goal is.
  • Read through the section Functions. Try typing any code you see in ipython or python
  • Come up with three questions as you are reading with your partner.
  • We’ll come around and help you.
  • We’ll regroup and you’ll teach me the slides.
  • We’ll solve the puzzle together.

Review

Defining a function:

def fun(x, y):
    z = x + y
    return z

x, y, z are local names

Local vs. Global

Symbols bound in Python have a scope

That scope determines where a symbol is visible, or what value it has in a given block.

In [14]: x = 32
In [15]: y = 33
In [16]: z = 34
In [17]: def fun(y, z):
   ....:     print(x, y, z)
   ....:
In [18]: fun(3, 4)
32 3 4

x is global, y and z local to the function

But, did the value of y and z change in the global scope?

In [19]: y
Out[19]: 33

In [20]: z
Out[20]: 34

In general, you should use global bindings mostly for constants.

In python we designate global constants by typing the symbols we bind to them in ALL_CAPS

INSTALLED_APPS = [u'foo', u'bar', u'baz']
CONFIGURATION_KEY = u'some secret value'
...

This is just a convention, but it’s a good one to follow.

Take a look at this function definition:

In [21]: x = 3

In [22]: def f():
   ....:     y = x
   ....:     x = 5
   ....:     print(x)
   ....:     print(y)
   ....:

What is going to happen when we call f

Try it and see:

In [23]: f()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-23-0ec059b9bfe1> in <module>()
----> 1 f()

<ipython-input-22-9225fa53a20a> in f()
      1 def f():
----> 2     y = x
      3     x = 5
      4     print(x)
      5     print(y)

UnboundLocalError: local variable 'x' referenced before assignment

Because you are binding the symbol x locally, it becomes a local and masks the global value already bound.

Parameters

So far we’ve seen simple parameter lists:

def fun(x, y, z):
    print(x, y, z)

These types of parameters are called positional

When you call a function, you must provide arguments for all positional parameters in the order they are listed

You can provide default values for parameters in a function definition:

In [24]: def fun(x=1, y=2, z=3):
   ....:     print(x, y, z)
   ....:

When parameters are given with default values, they become optional

In [25]: fun()
1 2 3

You can provide arguments to a function call for optional parameters positionally:

In [26]: fun(6)
6 2 3
In [27]: fun(6, 7)
6 7 3
In [28]: fun(6, 7, 8)
6 7 8

Or, you can use the parameter name as a keyword to indicate which you mean:

In [29]: fun(y=4, x=1)
1 4 3

Once you’ve provided a keyword argument in this way, you can no longer provide any positional arguments:

In [30]: fun(x=5, 6)
  File "<ipython-input-30-4529e5befb95>", line 1
    fun(x=5, 6)
SyntaxError: non-keyword arg after keyword arg

This brings us to a fun feature of Python function definitions.

You can define a parameter list that requires an unspecified number of positional or keyword arguments.

The key is the * (splat) or ** (double-splat) operator:

In [31]: def fun(*args, **kwargs):
   ....:     print(args, kwargs)
   ....:
In [32]: fun(1)
(1,) {}
In [33]: fun(1, 2, zombies=u"brains")
(1, 2) {'zombies': u'brains'}
In [34]: fun(1, 2, 3, zombies=u"brains", vampires=u"blood")
(1, 2, 3) {'vampires': u'blood', 'zombies': u'brains'}

args and kwargs are conventional names for these.

Documentation

It’s often helpful to leave information in your code about what you were thinking when you wrote it.

This can help reduce the number of WTFs per minute in reading it later.

There are two approaches to this:

  • Comments
  • Docstrings

Comments go inline in the body of your code, to explain reasoning:

if (frobnaglers > whozits):
    # borangas are shermed to ensure frobnagler population
    # does not grow out of control
    sherm_the_boranga()

You can use them to mark places you want to revisit later:

for partygoer in partygoers:
    for baloon in baloons:
        for cupcake in cupcakes:
            # TODO: Reduce time complexity here.  It's killing us
            #  for large parties.
            resolve_party_favor(partygoer, baloon, cupcake)

Be judicious in your use of comments.

Use them when you need to.

Make them useful.

This is not useful:

for sponge in sponges:
    # apply soap to each sponge
    worker.apply_soap(sponge)

In Python, docstrings are used to provide in-line documentation in a number of places.

The first place we will see is in the definition of functions.

To define a function you use the def keyword.

If a string literal is the first thing in the function block following the header, it is a docstring:

def complex_function(arg1, arg2, kwarg1=u'bannana'):
    """Return a value resulting from a complex calculation."""
    # code block here

You can then read this in an interpreter as the __doc__ attribute of the function object.

A docstring should:

  • be a complete sentence in the form of a command describing what the function does.
    • “”“Return a list of values based on blah blah”“” is a good docstring
    • “”“Returns a list of values based on blah blah”“” is not
  • fit onto a single line.
    • If more description is needed, make the first line a complete sentence and add more lines below for enhancement.
  • be enclosed with triple-quotes.
    • This allows for easy expansion if required at a later date
    • Always close on the same line if the docstring is only one line.

For more information see PEP 257: Docstring Conventions.

Recursion

You’ve seen functions that call other functions.

If a function calls itself, we call that recursion

Like with other functions, a call within a call establishes a call stack

With recursion, if you are not careful, this stack can get very deep.

Python has a maximum limit to how much it can recurse. This is intended to save your machine from running out of RAM.

Recursion is especially useful for a particular set of problems.

For example, take the case of the factorial function.

In mathematics, the factorial of an integer is the result of multiplying that integer by every integer smaller than it down to 1.

5! == 5 * 4 * 3 * 2 * 1

We can use a recursive function nicely to model this mathematical function

Functions Puzzle Solved!

Now it’s time to solve the puzzle. Remember:

In your local repo, after you’ve updated from upstream, go to session02 and find the file stackoverflow.py.

In it, you will find a function that calls itself.

  • What problems does this cause?
  • Why do you think the problem occurs?
  • How can you count the number of times a function can call itself?
  • Modify the program to implement your solution.

Boolean Expressions

Boolean Puzzle

  • Look up the % operator. What do these do?
    • 10 % 7 == 3
    • 14 % 7 == 0
  • Write a program that prints the numbers from 1 to 100 inclusive. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz” instead.
  • If you finish that, try your hand at writing solutions to one or more of the problems in codingbat.rst

Remember, Do These Steps

  • Read through the puzzle for that section.
  • Pick a partner. Describe what your goal is.
  • Read through the section Booleans. Try typing any code you see in ipython or python
  • Come up with three questions as you are reading with your partner.
  • We’ll come around and help you.
  • We’ll regroup and you’ll teach me the slides.
  • We’ll solve the puzzle together.

Truthiness

What is true or false in Python?

Determining Truthiness:

bool(something)
  • None
  • False
  • Nothing:
  • zero of any numeric type: 0, 0L, 0.0, 0j.
  • any empty sequence, for example, "", (), [].
  • any empty mapping, for example, {} .
  • instances of user-defined classes, if the class defines a __nonzero__() or __len__() method, when that method returns the integer zero or bool value False.
  • http://docs.python.org/library/stdtypes.html

Everything Else

Any object in Python, when passed to the bool() type object, will evaluate to True or False.

When you use the if keyword, it automatically does this to the statement provided.

Which means that this is redundant, and not Pythonic:

if xx == True:
    do_something()
# or even worse:
if bool(xx) == True:
    do_something()

Instead, use what Python gives you:

if xx:
    do_something()

and, or and not

Python has three boolean keywords, and, or and not.

and and or are binary expressions, and evaluate from left to right.

and will return the first operand that evaluates to False, or the last operand if none are True:

In [35]: 0 and 456
Out[35]: 0

or will return the first operand that evaluates to True, or the last operand if none are True:

In [36]: 0 or 456
Out[36]: 456

On the other hand, not is a unary expression and inverts the boolean value of its operand:

In [39]: not True
Out[39]: False

In [40]: not False
Out[40]: True

Because of the return value of these keywords, you can write concise statements:

                  if x is false,
x or y               return y,
                     else return x

                  if x is false,
x and y               return  x
                      else return y

                  if x is false,
not x               return True,
                    else return False
a or b or c or d
a and b and c and d

The first value that defines the result is returned

This is a fairly common idiom:

if something:
    x = a_value
else:
    x = another_value

In other languages, this can be compressed with a “ternary operator”:

result = a > b ? x : y;

In python, the same is accomplished with the ternary expression:

y = 5 if x > 2 else 3

PEP 308: (http://www.python.org/dev/peps/pep-0308/)

Boolean Return Values

Remember this puzzle from your CodingBat exercises?

def sleep_in(weekday, vacation):
    if weekday == True and vacation == False:
        return False
    else:
        return True

Though correct, that’s not a particularly Pythonic way of solving the problem.

Here’s a better solution:

def sleep_in(weekday, vacation):
    return not (weekday == True and vacation == False)

And here’s an even better one:

def sleep_in(weekday, vacation):
    return (not weekday) or vacation

In python, the boolean types are subclasses of integer:

In [1]: True == 1
Out[1]: True
In [2]: False == 0
Out[2]: True

And you can even do math with them (though it’s a bit odd to do so):

In [6]: 3 + True
Out[6]: 4

Boolean Puzzle Solved

Remember our puzzle:

  • Look up the % operator. What do these do?
    • 10 % 7 == 3
    • 14 % 7 == 0
  • Write a program that prints the numbers from 1 to 100 inclusive. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz” instead.
  • If you finish that, try your hand at writing solutions to one or more of the problems in codingbat.rst

Volunteer to upload your solution to Slack!

Code Structure, Modules, and Namespaces

Scopes within scopes, attributes within attributes

Module Puzzle

Write a module (file) called mystery.py with a function inside that solves one of the CodingBat exercises from before:

codingbat.rst

Be sure to write a good docstring for your function describing how to use it, like this example.

def square_root(n):
    """
    Calculate the square root of a number.

    Args:
        n: the number to get the square root of.
    Returns:
        the square root of n.

    """
    pass

Include a check to see if the module is being run, or it is being imported.

If it is being run, execute some test code that calls your function.

Remember, Do These Steps

  • Read through the puzzle for that section.
  • Pick a partner. Describe what your goal is.
  • Read through the section Code Structure, Modules, Namespaces. Try typing any code you see in ipython or python
  • Come up with three questions as you are reading with your partner.
  • We’ll come around and help you.
  • We’ll regroup and you’ll teach me the slides.
  • We’ll solve the puzzle together.

Code Structure

In Python, the structure of your code is determined by whitespace.

How you indent your code determines how it is structured

block statement:
    some code body
    some more code body
    another block statement:
        code body in
        that block

The colon that terminates a block statement is also important...

You can put a one-liner after the colon:

In [167]: x = 12
In [168]: if x > 4: print(x)
12

But this should only be done if it makes your code more readable.

Whitespace is important in Python.

An indent could be:

  • Any number of spaces
  • A tab
  • A mix of tabs and spaces:

If you want anyone to take you seriously as a Python developer:

Always use four spaces – really!

(PEP 8)

Other than indenting – space doesn’t matter, technically.

x = 3*4+12/func(x,y,z)
x = 3*4 + 12 /   func (x,   y, z)

But you should strive for proper style. Read PEP 8 and install a linter in your editor.

Modules and Packages

Python is all about namespaces – the “dots”

name.another_name

The “dot” indicates that you are looking for a name in the namespace of the given object. It could be:

  • name in a module
  • module in a package
  • attribute of an object
  • method of an object

A module is simply a namespace.

It might be a single file, or it could be a collection of files that define a shared API.

To a first approximation, you can think of the files you write that end in .py as modules.

A package is a module with other modules in it.

On a filesystem, this is represented as a directory that contains one or more .py files, one of which must be called __init__.py.

When you have a package, you can import the package, or any of the modules inside it.

import modulename
from modulename import this, that
import modulename as a_new_name
from modulename import this as that
import packagename.modulename
from packagename.modulename import this, that
from package import modulename

http://effbot.org/zone/import-confusion.htm

from modulename import *

Don’t do this!

Import

When you import a module, or a symbol from a module, the Python code is compiled to bytecode.

The result is a module.pyc file.

This process executes all code at the module scope.

For this reason, it is good to avoid module-scope statements that have global side-effects.

The code in a module is NOT re-run when imported again

It must be explicitly reloaded to be re-run

import modulename
reload(modulename)

In addition to importing modules, you can run them.

There are a few ways to do this:

  • $ python hello.py – must be in current working directory
  • $ python -m hello – any module on PYTHONPATH anywhere on the system
  • $ ./hello.py – put #!/usr/bin/env python at top of module (Unix)
  • In [149]: run hello.py – at the IPython prompt – running a module brings its names into the interactive namespace

Like importing, running a module executes all statements at the module level.

But there’s an important difference.

When you import a module, the value of the symbol __name__ in the module is the same as the filename.

When you run a module, the value of the symbol __name__ is __main__.

This allows you to create blocks of code that are executed only when you run a module

if __name__ == '__main__':
    # Do something interesting here
    # It will only happen when the module is run

This is useful in a number of cases.

You can put code here that lets your module be a utility script

You can put code here that demonstrates the functions contained in your module

You can put code here that proves that your module works.

Writing tests that demonstrate that your program works is an important part of learning to program.

The python assert statement is useful in writing main blocks that test your code.

In [1]: def add(n1, n2):
   ...:     return n1 + n2
   ...:

In [2]: assert add(3, 4) == 7

In [3]: assert add(3, 4) == 10
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-3-6731d4ac4476> in <module>()
----> 1 assert add(3, 4) == 10

AssertionError:

In-Class Lab

Import Interactions

Exercises

Experiment with importing different ways:

In [3]: import math

In [4]: math.<TAB>
math.acos       math.degrees    math.fsum       math.pi
math.acosh      math.e          math.gamma      math.pow
math.asin       math.erf        math.hypot      math.radians
math.asinh      math.erfc       math.isinf      math.sin
math.atan       math.exp        math.isnan      math.sinh
math.atan2      math.expm1      math.ldexp      math.sqrt
math.atanh      math.fabs       math.lgamma     math.tan
math.ceil       math.factorial  math.log        math.tanh
math.copysign   math.floor      math.log10      math.trunc
math.cos        math.fmod       math.log1p
math.cosh       math.frexp      math.modf
In [6]: math.sqrt(4)
Out[6]: 2.0
In [7]: import math as m
In [8]: m.sqrt(4)
Out[8]: 2.0
In [9]: from math import sqrt
In [10]: sqrt(4)
Out[10]: 2.0

Experiment with importing different ways:

import sys
print(sys.path)
import os
print(os.path)

You wouldn’t want to import * those!

– check out
os.path.split(u'/foo/bar/baz.txt')
os.path.join(u'/foo/bar', u'baz.txt')

Module Puzzle Solved

Now we will solve our Module Puzzle!

Write a module (file) called mystery.py with a function inside that solves one of the CodingBat exercises from before:

codingbat.rst

Be sure to write a good docstring for your function describing how to use it, like this example.

def square_root(n):
    """
    Calculate the square root of a number.

    Args:
        n: the number to get the square root of.
    Returns:
        the square root of n.

    """
    pass

Include a check to see if the module is being run, or it is being imported.

If it is being run, execute some test code that calls your function.

Someone upload their file to Slack and volunteer.

I’ll go through the process of importing the module, and we’ll try to figure out what your function does, and how to run it.

Homework

Two Tasks by Monday

Task 4

The Fibonacci Series is a numeric series starting with the integers 0 and 1. In this series, the next integer is determined by summing the previous two. This gives us:

0, 1, 1, 2, 3, 5, 8, 13, ...

Create a branch in your local repo called task4 and switch to it (git checkout task4).

Create a session02 folder in your student folder. For example, mine would have the path students/PaulPham/session02.

Create a new module series.py in the session02 folder in your student folder. In it, add a function called fibonacci. The function should have one parameter n. The function should return the nth value in the fibonacci series, starting at 0.

For example, fibonacci(n=0) should equal 0. fibonacci(n=1) should equal 1. fibonacci(n=2) should equal 1. And so forth.

Ensure that your function has a well-formed docstring

The Lucas Numbers are a related series of integers that start with the values 2 and 1 rather than 0 and 1. The resulting series looks like this:

2, 1, 3, 4, 7, 11, 18, 29, ...

In your series.py module, add a new function lucas that returns the nth value in the lucas numbers

Ensure that your function has a well-formed docstring

Both the fibonacci series and the lucas numbers are based on an identical formula.

Add a third function called sum_series with one required parameter and two optional parameters. The required parameter will determine which element in the series to print. The two optional parameters will have default values of 0 and 1 and will determine the first two values for the series to be produced.

Calling this function with no optional parameters will produce numbers from the fibonacci series. Calling it with the optional arguments 2 and 1 will produce values from the lucas numbers. Other values for the optional parameters will produce other series.

Ensure that your function has a well-formed docstring

Add an if __name__ == "__main__": block to the end of your series.py module. Use the block to write a series of assert statements that demonstrate that your three functions work properly.

Use comments in this block to inform the observer what your tests do.

Add your new module to your local repo (on branch task4) and commit frequently while working on your implementation. Include good commit messages that explain concisely both what you are doing and why.

Add your files to that branch, commit and push, then submit a pull request to the main class repo.

When you are finished, push your changes to your fork of the class repository in GitHub. Finally, submit your assignment in Canvas by giving the URL of the pull request.

Task 5

Read through the Session 03 slides.

http://codefellows.github.io/sea-c34-python/session03.html

There are three sections:

  • Sequences
  • Iteration
  • String Formatting

For each section, come up with three questions and write some Python code to help you answer them, one function per question.

For each function, write a good docstring describing what question you are trying to answer.

Put the functions in three separate modules (files) called sequences.py, iteration.py, and string.py in the session02 subdirectory of your student directory, just as you did for series.py up above.

That is, you should have nine questions, and nine functions, total, spread out across three files.

Use everything you’ve learned so far (including functions, booleans, and printing).

Create a branch in your local repo called task5 and switch to it (git checkout task5).

Add your files to that branch, commit and push, then submit a pull request to the main class repo.

Finally, submit your assignment in Canvas by giving the URL of the pull request.