Into the weeds

Python fundamentals

Statements

  • A statement is a complete Python instruction

  • Two kinds of statements

    • Simple

    • Compound

Simple statements are individual instructions

x = 0
continue
return True
print("Hello")

Compound statements contain other statements

for word in wordlist:
    print(word)
    first_letter = word[0]
if score >= 5:
    print("You win!")
else:
    print("Better luck next time.")

Anatomy of a compound statement

ComplexStatement

Summary

A statement is a complete Python instruction

  • Simple vs. compound statements

  • Elements of a compound statement

    • Clause

    • Header

    • Body

      • The body of a compound statement is always indented relative to the header

Expressions

  • An expression is a chunk of code that evaluates to a value

  • Expressions appear in many Python statements

    • Anywhere Python calls for a value, you can provide an expression

  • Any expression that is a complete line of code is also a statement

Common types of expressions

  • Literal value (5, "hi", True)

    • evaluates to the value

  • Variable

    • evaluates to the value of the variable

  • Function call (abs(-4), round(10.6))

    • evaluates to the result of the function call

Operators allow you to make more complex expressions

  • + addition

  • - subtraction

  • * multiplication

  • / floating-point division

  • // floor division

  • % modulo operation

  • ** exponentiation

  • == equality

  • != inequality

  • > greater than

  • < less than

  • >= greater than or equal

  • <= less than or equal

  • not logical negation

  • and logical conjunction

  • or logical disjunction

  • in membership

  • is identity

Division operators

  • / performs "regular" (floating-point) division

    • 5 / 4 evaluates to 1.25

    • Floating-point math can be imprecise

  • // performs floor division (rounds down to the nearest integer)

    • 5 // 4 evaluates to 1

    • -5 // 4 evaluates to -2

  • % gives you the remainder after division

    • 5 % 4 evaluates to 1

    • -5 % 4 evaluates to 3

    • Can be used to test parity (even/odd):
      x % 2 is 0 if x is even, 1 otherwise

Parentheses

  • Parentheses are used for grouping, like in algebra

  • You can put parentheses around any expression

  • A parenthesized expression can contain line breaks

    (  3 * x**3
     + 4 * x**2
     - 2 * x
     + 1)

Summary

An expression is a chunk of code that evaluates to a value

  • Simple expressions include literal values, variables, and function calls

  • You can use operators to make more complex expressions

    • Mind the differences between division operators (/, //, %)

  • Parentheses in expressions are used for grouping (as in algebra)

Variables

  • Variables in Python are labels attached to values

    • Multiple labels can be attached to the same value

  • Variable names can consist of letters, numbers, and underscores

    • Variable names can’t start with numbers

    • Can’t use keywords (if, return, def, etc.) as variable names

    • Shouldn’t use function names (print, list, round, etc.) as variable names

    • By convention, avoid capital letters in variable names

Variable assignment

variable = expression

name = "Gina"
area = width * height
nearest_int = round(5.3)

Variable assignment statements are not expressions

Augmented assignment

Common task:

  • Take a variable

  • Do some operation on the value of the variable

  • Assign the result of the operation to the variable

Python gives us a set of augmented assignment operators to simplify this process:

+=

-=

*=

**=

/=

//=

%=

Example:

x = x + 1

Example:

x += 1

Summary

Variables in Python are labels attached to values

  • Variable assignment is usually done with =

  • Variables can be updated with augmented assignment operators (e.g., +=)

Conditional statements

  • if clause is mandatory, comes first

    • if condition:
          body

  • Zero or more elif clauses

    • elif condition:
          body

  • Zero or one else clause

    • else:
          body

if temp <= 0:
    water_state = "ice"
elif temp <= 100:
    water_state = "water"
else:
    water_state = "steam"


  • At most one clause gets executed

  • Conditions in a conditional statement are expressions

elif vs. if

Compare:

if temp <= 0:
    water_state = "ice"
elif temp <= 100:
    water_state = "water"
else:
    water_state = "steam"
if temp <= 0:
    water_state = "ice"
if temp <= 100:
    water_state = "water"
else:
    water_state = "steam"

What if the value of temp is

  • 120?

  • 15?

  • -10?

Boolean contexts

  • Conditions are Boolean contexts: Python converts the result of these expressions to True or False

  • Most values are "truthy". The following are "falsy":

    • 0 and 0.0

    • Empty collections (e.g., empty string, empty list, empty dictionary)

    • None

Example: non-Boolean value in a Boolean context

answer = input("What is your favorite color? ")
if answer:
    print("You entered:", answer)
else:
    print("You didn't enter anything")

Conditional expressions

expression1 if condition else expression2

status = "minor" if age < 18 else "adult"
print("¡Hola!" if lang="es" else "Hello!")


Note that expression2 can be another conditional expression:

water_state = ("ice"   if temp <=   0 else
               "water" if temp <= 100 else
               "steam")

Conditional statements vs. conditional expressions

Conditional statements execute code if certain conditions are met; elif and else clauses are optional

Conditional expressions evaluate to one of two values depending on a condition; else component is required

if today == user_birthday:
    print("Happy birthday!")
else:
    print("Have a nice day!")
print("Happy birthday!"
      if today == user_birthday
      else "Have a nice day!")

Summary

  • Conditional statements execute code conditionally

  • Conditional expressions evaluate to one of two values depending on a condition

  • Conditions are Boolean contexts

Loops

Loops let us repeat instructions. Two kinds:

  • for loops: number of iterations is known in advance

  • while loops: number of iterations is not known in advance

for loops

for varname in iterable:
    body

print("The first ten"
      " triangular numbers:")
t = 0
for i in range(1, 11):
    t += i
    print(t)
  • iterable is a sequence (such as a string or a list) or sequence-like object (such as a range object).

  • varname is called the iteration variable. It is created and populated by the for loop.

  • There will be one iteration of the loop for every item in iterable.

while loops

while condition:
    body

n = 1
while n < 1000:
    n *= 3
print("The first power of 3 greater"
      " than 1000 is", n)
  • condition is an expression.

  • The loop iterates as long as condition evaluates to True (or another truthy value).

Altering flow of control in a loop

  • continue ends the current iteration of a loop. The loop moves on to the next iteration, if there is one; otherwise it terminates.

  • break causes a loop to terminate. Control passes to the code after the loop.

  • return terminates both a loop and the function it is in. Control passes to the statement that called the function.

while True:

  • Ensures the body of a loop executes at least once

  • Loop should contain break or return so that it is not infinite

def get_selection():
    """ Ask the user to select 'a' or 'b'. Return their selection. """
    while True:
        response = input("Choose 'a' or 'b': ")
        if response in ['a', 'b']:
            return response
        # if we didn't return, the user made an invalid choice
        print("Sorry, that's not one of the choices.")

Example with continue and break

Imagine we need to extract some information from a file. Here are the rules we will follow to get the information we need:

  • Ignore lines that start with #

  • Stop reading the file if we encounter a line consisting of four hyphens

  • Make a list containing the text of other lines in the file

data = list()
f = open('somefile.txt')
for line in f:
    line = line.strip()
    if line[0] == "#":
        continue
    if line == "----":
        break
    data.append(line)
f.close()

Summary

Loops let us repeat instructions

  • for loops iterate a definite number of times

  • while loops iterate until a condition is false/falsy

    • while True guarantees at least one iteration

  • The flow of control in a loop can be altered using

    • continue

    • break

    • return (only if the loop is in a function)

Functions

  • Reusable sequences of instructions packaged as a unit

  • Can take input values (arguments)

  • Can return a value

Some built-in functions you should know:

  • print

  • abs

  • round

  • float

  • int

  • str

Calling functions

  • All function calls include the function name followed by parentheses

  • Many functions take arguments; these go inside the parentheses

    • Arguments can be expressions

  • Function calls are expressions; they evaluate to the return value of the function

Examples:

print()              # function call with no arguments
x = abs(-10)         # function call with one argument
n = round(14.723, 2) # function call with two arguments

Two kinds of arguments

print("Hello", "world!", sep="::", end="\n--\n")
  • "Hello" and "world!" are positional arguments

  • sep="::" and end="\n--\n" are keyword arguments

    • Keyword arguments always have a keyword, an equal sign, and a value/expression

  • Positional arguments always precede keyword arguments

  • The order of positional arguments is important

  • The order of keyword arguments isn’t important

Defining functions

Breaking a program into small functions is a Really Good Idea

  • Functions help you break down a problem into discrete steps

  • Functions make a program easier to

    • Read

    • Test and debug

    • Reuse

Defining functions

def function(parameters):
    """ docstring """
    body

def dist(x1, y1, x2, y2):
    """ Calculate the Euclidean distance
    between coordinates (x1, y1) and
    (x2, y2). """
    return ((x1 - x2)**2 +
            (y1 - y2)**2)**0.5
  • function is the name of the function.

  • parameters (optional) are variables that get their values when the function is called.

  • A docstring (optional) describes what the function does and how to use it.

  • The body contains the instructions that constitute the function. It usually contains a return statement.

Variable scope

Two kinds of variables (for now):

  • Global variables

    • Usually defined outside of any function

    • Can be accessed anywhere in a program

    • Functions that use global variables are not self-contained

    • Can lead to bugs that are hard to track down

  • Local variables

    • Defined within a function (includes parameters)

    • Can only be accessed within the function where they are defined

    • Less risk of weird bugs

Local variables are a Really Good Thing

Two kinds of parameters

  • Required parameters are just variable names. The function won’t work unless an argument is provided for each required parameter.

  • Optional parameters look like keyword arguments: parameter=default_value. If the user omits an optional parameter, the function will still work and the parameter’s value will be the default value.

    • Optional parameters always follow required parameters.

    • Avoid mutable values as default values.

def get_pace(dist, time, dist_unit="mile", time_unit="minute"):
    """ Calculate a pace given a distance and time. """
    print(time/dist, time_unit + "s per", dist_unit)

Calling functions with optional parameters

def get_pace(dist, time, dist_unit="mile", time_unit="minute"):
    """ Calculate a pace given a distance and time. """
    print(time/dist, time_unit + "s per", dist_unit)
get_pace(3, 54)
18.0 minutes per mile
get_pace(6, 0.9, time_unit="hour")
0.15 hours per mile
get_pace(16, 0.8, dist_unit="kilometer", time_unit="hour")
0.05 hours per kilometer
get_pace(16, 0.8, time_unit="hour", dist_unit="kilometer")
0.05 hours per kilometer

return vs. print()

return defines what your function will evaluate to when it is called

print() writes information to the console (or to a file or file-like object)

return communicates information to another part of a program

print() (usually) communicates information to the user

returned values are easy to test

print()ed values are harder to test

Most functions should use return rather than print()

If it’s important to show the user the result of a function, call the function, then print() the return value

Can you make sense of this?

def f(n):
    if not isinstance(n, int):
        raise TypeError("n must be an integer")
    if n < 2:
        raise ValueError("n must be at least 2")
    fs = []
    for i in range(2, int(n**0.5)+1):
        while n % i == 0:
            fs.append(i)
            n //= i
    if n != 1:
        fs.append(n)
    return fs

Can you make sense of this?

def factorize(n):
    """Find all prime factors of n.

    Args:
        n (int): number to factorize. Must be greater than 1.

    Returns:
        list of int: a list of all prime factors of n. The product of
        these numbers will be equal to n. Factors will be ordered from
        smallest to largest.

    Raises:
        TypeError: value of n is not an integer.
        ValueError: n is less than 2.
    """
    if not isinstance(n, int):
        raise TypeError("n must be an integer")
    if n < 2:
        raise ValueError("n must be at least 2")

    factors = []
    for i in range(2, int(n**0.5)+1):
        while n % i == 0:
            factors.append(i)
            n //= i
    # if n is not 1, it is also a prime factor
    if n != 1:
        factors.append(n)
    return factors

Docstrings

  • Purpose of function

  • Purpose of arguments and expected data types

  • Meaning and data type of return value

  • Exceptions the function might raise

  • Side effects the function might have

"""Find all prime factors of n.

Args:
    n (int): number to factorize. Must
        be greater than 1.

Returns:
    list of int: a list of all prime
        factors of n. The product of
        these numbers will be equal to
        n. Factors will be ordered from
        smallest to largest.

Raises:
    TypeError: value of n is not an
        integer.
    ValueError: n is less than 2.
"""

Summary

Functions are reusable sequences of instructions packaged as a unit

  • Arguments are values passed to functions as input

    • Positional arguments

    • Keyword arguments

  • Parameters are variables defined in a function header whose values correspond to the arguments

    • Required parameters

    • Optional parameters

  • Variables have scope

  • Docstrings communicate how to use a function

Exceptions

When something goes wrong in a Python program, an exception is raised. Common exceptions include

  • IndexError: the specified index (e.g., of a list) is out of range

  • KeyError: the specified key (in a dictionary) is undefined

  • NameError: the specified name (of a variable, function, etc.) is undefined

  • ValueError: the specified value is incompatible with the requested operation or function (e.g., int("hello"))

  • ZeroDivisionError: the code tried to divide by zero

Stack traces

CallStack01

Stack traces

CallStack02

Stack traces

CallStack03

Stack traces

CallStack04

Stack traces

CallStack05

Stack traces

CallStack06

Stack traces

CallStack07

Stack traces

CallStack08

Stack traces

CallStack09

Stack traces

CallStack10

Stack traces

CallStack11

Stack traces

CallStack12

Stack traces

CallStack13

Stack traces

CallStack14

Stack traces

CallStack15

Stack traces

CallStack16

Stack traces

CallStack17

Stack traces

CallStack18

Stack traces

CallStack19

Stack traces

CallStack20

Stack traces

CallStack21

Stack traces

CallStack22

Stack traces

CallStack23

Raising exceptions

Your code can raise exceptions using a raise statement:

if response not in ['a', 'b', 'c']:
    raise ValueError("Response should be 'a', 'b', or 'c'")

A raise statement consists of the keyword raise and an exception. It’s a good idea to include an error message that explains why the exception got raised.

Handling exceptions

  • try clause is mandatory, comes first

    • try:
          body

  • Zero or more except clauses

    • except:
          body

      • Handles any exception; not recommended

    • except exception:
          body

    • except exception as varname:
          body

  • Optional else clause

    • else:
          body

      • Applies when no exception is encountered

  • Optional finally clause

    • finally:
          body

      • Applies whether or not an exception was encountered

  • A try statement needs a finally clause or at least one except clause

Exception handling example: input validation

def get_int():
    """ Ask the user for an integer.
    Repeat until the user enters a valid integer. """
    while True:
        response = input("Enter an integer: ")
        try:
            number = int(response)
        except ValueError:
            print("The value you entered is not an integer; please try again")
        else:
            return number

Summary

An exception is raised when something goes wrong in a Python program

  • Exceptions propagate up the call stack

  • A stack trace gives you useful information for debugging

  • The raise statement allows your code to raise its own exceptions

  • Exceptions can be handled using try statements