## Week 1: Course Overview and Python Basics¶

### Video 1.1: Course Logistics and Overview¶

The course website contains all important information about the course:

    - Syallabus
    - Course Calendar
    - Assignments
    - Remote Learning: Panopto Videos
    - Remote Learning: Zoom live Videos
    - Piazza
    - Gitlab (Repositories)

Course Website: http://people.cs.uchicago.edu/~lamonts/classes/2019_2020/fall/mpcs51042/index.html

## Video 1.2: Python Introduction¶

### Video Outline¶

1. Why learn Python?
2. How to run a Python program?

### Section #1.2.1: Why Learn Python?¶

In my opinion, here are the reasons to learn Python:

1. Easy to learn:

   Python's syntax is relatively simple with very few rules and special cases. The syntax allows for rapid prototyping, which allows for you to focus on what you want to do with the code and quickly experiment and implement ideas. You typically spend less time in Pythong worrying about language intricacies.

1. It is an open source, general-purpose and multiparadigm programming language:

   Allows for implementing applications in many different domains (e.g., data science, artificial intelligence, web development, systems level scripting, etc). With more than 20 years of development, it is available on many different platforms (Linux, Windows, MacOS, etc.) and works as intended on these platforms. It also allows programmers to develop code using a combination of different programming paradigms such as procedural, object-oriented, and functional programming.

1. Continues to be a popular language:

   -- https://stackoverflow.blog/2017/09/06/incredible-growth-python/

-- https://stackoverflow.blog/2017/09/06/incredible-growth-python/

1. You want a job in Data Science and Artificial Intelligence (especially Machine learning):

   Python is the preferred programming languages in many regards in the data ccience and machine learning fields. With its vast number of packages in these areas, other languages cannot match the level of sophicastion of these packages bring to the language.

Note: There are areas where using Python would probably not be appropriate. Throughout this course, I'll make sure to talk about the pitfalls of the language as well.

### Section 1.2.2 : Running Python Programs¶

When learning new languages, the traditional first program is the one that prints the text "hello world" to the terminal. Here's the implemenetation in Python.

In [5]:

    # This will print "Hello World" to the console
    print("Hello World")

    Hello World

Notes:

- `print` is a built-in function. It takes a value and prints it to the terminal.
- Comments (i.e., code documentation) is done via `#`.

#### Running this program¶

In Python, there are two ways of running code:

1. Using an interactive prompt via the Python interpreter (i.e., a program that executes other programs. Using the interpreter in its interactive mode, will execute the instructions (i.e., the statements in your code) one by one. You write a line of code, you hit enter, and the interpreter executes that line of code and shows the result (if any).

   - The interpreter is accessible via the terminal program on your machine.

   - The interpreter is also known as a RELP (read, eval, print, loop)

   - The REPL is great for learning, experimenting, and testing Python code

- Demo: Using the IPython interpreter to print `Hello World`

1. Saving your code in a file and then running that file using the interpreter.

- Demo: Create a blank file called `hello.py`, write `print("Hello, world!")` inside it, and then run::

      ipython hello.py

Note: We will discuss how the interpter works in the coming weeks.

#### Python Programs¶

As stated before, the REPL a good tool for experimenting with code interactively. Once your code becomes complex then you will want to save it as a Python program. These programs are saved in a `.py` file (or a collection of `.py` files).

#### Aside: Python Versions¶

Python2 and Python3 are the two main versions of Python. However, the end of life for Python2 happend at the beginning of 2020; therefore, Python3 is the future of the language and is the one we will use.

## Video 1.3: Python Basics¶

### Video Outline¶

1. Variables & Types in Python
2. Numeric Types
3. Sequence Types

### Section #1.3.1: Python Basics¶

You all have prior programming experience; therefore, I want to quickly go over the syntax for common programming constructs in Python.

We will learn about more advance features of these constructs over the next few weeks. For now, I will just show you the basic syntax for them.

#### Variables¶

A variable in Python has the following syntax:

`variable_name = expression`

Notes:

- Each `variable_name` should be in all lowercase letters with words seperated by an underscore.
- Constant variables should be all uppercase characters with words seperated by an underscore (`CONSTANT_NAME`).
- Remember an `expression` is just a combination of one or more constants, variables, operators, functions, etc.

In [6]:

    x = 1234

In [7]:

    y = True

In [8]:

    z = 4

In [12]:

    var = x + z
    var

Out[12]:

    1238

#### Types¶

Notice how I did not specify the type for the above variables. This is because Python is a dynamically-typed language: it automatically determines and keeps track of varible types without requiring a type declaration. Other languages, like Java and C/C++, are statically-typed and require the programmer to specify the type of every variable. For example, in Java we would need to declare the variable `x` from above as:

    int x = 1234;

At any point you can determined the infered type for a variable by using the `type(variable_name)` function.

In [13]:

    type(x)

Out[13]:

    int

In [14]:

    type(y)

Out[14]:

    bool

We will learn more about Python's type system next week.

#### Core Data Types¶

As with other programming languages, Python provides built-in core types:

- Numeric types: `int`, `float`, `complex`
- Sequence types: `str`, `list`, `tuple`
- Boolean: `bool`
- Set types: `set`
- Mapping types: `dict`

In week #1, we will cover the numeric, sequence, and boolean types. Next week, we will talk about the set and mapping types.

#### Numeric Types¶

The following diagram provides examples of numeric values in Python:

-- Learning Python 2013

- Specialized numeric types (e.g., Fraction Decimal) can be imported from the standard library.

- Explicit conversion of the numeric types are done using the `int()`, `float()` and `complex()` functions.

In [15]:

    x = 235
    x

Out[15]:

    235

In [16]:

    y = float(x) #convert the integer value for x into a float value
    y

Out[16]:

    235.0

#### Numeric Expressions/Operators¶

The following diagram provides examples of defining numeric expressions using the common numeric operators provided in Python:

-- Learning Python 2013

Note: the evaluation of numeric expressions are done similiarly to the order of precedence in other languages:

Order of Precedence in Python:https://docs.python.org/3/reference/expressions.html#summary

#### Arithmetic Conversion¶

Mixed arithmetic is allowed. Python converts operands up to the type of the most complicated operand. Converting up is also done at the expression level.

- Highest to lowest: complex numbers -> floating point numbers -> integers

In [17]:

    34 + 3.2 # Conversion = 34.0 + 3.2 = 37.2

Out[17]:

    37.2

In [18]:

    (3 + 5) - 4.0 # Conversion = (3.0 + 5.0) - 4.0 = 4.0

Out[18]:

    4.0

#### Boolean type¶

As with other languages, the values that represent booleans are defined as:

- Case-sensitive truth value: `True`
- Case-snesitive false value: `False`

#### Relational Operators¶

- Operators that tests or defines some kind of relation between two values.

Syntax | Definition
--- | ---
`x > y` | `True` if left operand is greater than the right
`x < y` | `True` if left operand is less than the right
`x == y` | `True` if both operands are equal
`x != y` | `True` if both operands are not equal
`x >= y` | `True` if left operand is greater than or equal to the right
`x <= y` | `True` if left operand is less than or equal to the right

In [19]:

    3 <= 12

Out[19]:

    True

In [20]:

    5 == 7

Out[20]:

    False

#### Logical Operators¶

- Operators that perform logical AND, logical OR, and logal NOT
- These operators short-circuit (i.e., when an expression is stopped being evaluated as soon as its outcome is determined.)

Syntax | Definition
--- | ---
`x and y` | `True` if both the operands are true
`x or y` | `True` if either of the operands is true
`not x` | `True` True if operand is false

In [21]:

    3 <= 12 and 4 >= 4

Out[21]:

    True

In [22]:

    b = False
    not b

Out[22]:

    True

#### None Type¶

As with many other languages, there is a value in Python that represents the absence of a value. In Python, this is known as the None type and has a single value as `None`.

We will talk more about the use cases of None as we progress in the course.

In [23]:

    x = None
    print(x)

    None

#### Sequence types¶

Sequences allow you to store multiple values in an organized and effcient way. The sequence types we will examine will be `str`, `list`, and `tuple`.

#### Strings¶

- Strings (i.e., `str` type in Python) are used to record both textual information and arbitrary collections of bytes.

- String are immutable sequences: positionally ordered collection of other objects that cannot be modified.

- String transformation operations/functions always produce new `str` values.

There are many different ways of defining a `str` in Python

In [18]:

    # Single quote literal
    # Allows you to embed double quotes (") inside the string
    str1 = 'spa"m'
    print(str1)
    type(str1)

    spa"m

Out[18]:

    str

In [24]:

    # Double quote literal
    # Allows you to embed double quotes (') inside the string
    str2 = "spa'm"
    print(str2)

    spa'm

In [25]:

    # Triple quote literal (multi-line strings)
    # Multi-line strings include the newlines
    str3 = '''Here is a list of names all on different lines
    ""Bob"
    "Sally"
    "Joe"
    "Karen"'''
    print(str3) # Notice the newlines are preserved

    Here is a list of names all on different lines
    ""Bob"
    "Sally"
    "Joe"
    "Karen"

In [26]:

    # Double quote version (multi-line strings)
    # Allows for escaping single quotes ''
    str3 = """Here is a list of names all on different lines
    'Bob'
    'Sally'
    'Joe'
    'Karen'"""
    print(str3)

    Here is a list of names all on different lines
    'Bob'
    'Sally'
    'Joe'
    'Karen'

In [27]:

    # As with other languages, you can have escape characters in strings
    # \t - tab character
    # \n - newline character
    # Many others: https://www.w3schools.com/python/gloss_python_escape_characters.asp
    str5 = "Bob\tSally\nJoe"
    print(str5)
    str6 = 'Bob\tSally\nJoe'

    Bob	Sally
    Joe

In [23]:

    #Below is an example of "raw strings", which include escape characters
    # a raw string always begins with a "r" before the string syntax
    str7 = r'C:\new\test.py'
    non_raw_str7 = 'C:\new\test.py'

    #Notice the strings actually contain the \n and \t in the ouput versus
    # the non-raw versions
    print(str7)
    print("----")
    print(non_raw_str7)

    C:\new\test.py
    ----
    C:
    ew	est.py

#### String Formatting¶

Another way to define strings is to format them. Formattting allows you to incorporate values from other types to produce a new string.

The following are a few ways to produce a formatted string.

In [28]:

    # By relative position '{}' using the "format" method.
    # Auto-conversion for non-strings.
    f_str = 'Address = {} {} {}'.format(4543, 'Elaine', 'Street')
    print(f_str)

    Address = 4543 Elaine Street

In [29]:

    # By absolute position '{index}', where index is an integer
    f_str2 = 'Hello {2}, {0} and Bye {1}'.format('Bob','Sally','Joe')
    print(f_str2)

    Hello Joe, Bob and Bye Sally

In [30]:

    # By keyword. Specify keyword first followed by the value.
    f_str3 = 'Address = {num} {name} {suffix}'.format(num = 4543, name = 'Elaine', suffix = 'Street')
    print(f_str3)

    Address = 4543 Elaine Street

In [33]:

    # New in Python 3.6+: formatted string literals.
    # IMPORTANT - Note the 'f'. This 'f' is required!

    #Define a few variables
    num = 4543
    name = 'Elaine'
    suffix = 'Street'

    # Directly place your expressions inside the curly braces
    f_str4 = f'Address = {num} {name} {suffix}'
    print(f_str4)

    Address = 4543 Elaine Street

The string formatting method includes additional options for formatting:

- Precision: number of decimals for float objects
- Hex, binary, or octal representation for ints
- Justification options and whitespace padding

Documentation:

- Learning Python Chapter 7, ”String Formatting Method Calls": https://docs.python.org/3.4/library/string.html#format-specification-mini-language

#### Lists¶

One of the most useful sequence types are lists (`list`) and they are:

- A great data structure if you need to hold a collection of positionally-ordered and arbitrarily-typed values.

- Mutable (i.e., they can be modified in-place)

- Dynamically Resizable (i.e., they can be shorten or extended as needed)

#### List Creation¶

There are many different ways to define a list in the Python. The following code sections provide a few ways.

In [34]:

    # Empty List
    lst = []
    lst

Out[34]:

    []

In [35]:

    # Defining a list with specified items
    # Remember a list can contain values of different types.
    lst = [123, 'abc', 1.23]
    lst

Out[35]:

    [123, 'abc', 1.23]

In [36]:

    # Nested lists (lists within lists)
    lst = ['Bob', 40.0, ['dev', 'mgr']]
    lst

Out[36]:

    ['Bob', 40.0, ['dev', 'mgr']]

#### Tuples¶

A `tuple` is an immutable sequence of arbitrary objects that are also:

- Fixed-length
- Heterogeneous
- Arbitrarily nestable

#### Tuple Creation¶

There are many different ways to define a tuple in the Python. The following code sections provide a few ways.

In [37]:

    # Empty Tuple
    tup = ()
    tup

Out[37]:

    ()

In [38]:

    # A one-item tuple (not an expression)
    tup = (0,)
    tup
    # Asside: Why couldn't we just use say (0)?

Out[38]:

    (0,)

In [39]:

    # A four-item tuple
    tup = (0, 'Ni', 1.2, 3)

    # Another four-item tuple (same as prior line) without the need for the parenthesis
    tup2 = 0, 'Ni', 1.2, 3

    print(tup)
    print(tup2)

    (0, 'Ni', 1.2, 3)
    (0, 'Ni', 1.2, 3)

Aside: It seems that lists and tuples are the same. They both hold a collection of values but they have slightly different syntax. When should you use one over the other? We will talk about this next week.

#### Sequence Operations¶

All sequence types (e.g., `str`, `list`, `tuple`) support a series of useful operations:

- Length (`len`):

      - Determines the size of a sequence value.
      - The result is an integer value.

- Concatenation(`+`):

  - Combine sequence values together
  - The result is a new sequence value

- Repetition(`*`):

  - Repeat a sequence value a certain number times
  - The result is a new sequence value

- Containment(`in`):

  - Determines whether a sequence value is inside another one
  - The result is a boolean value.

In [40]:

    # Length

    # Strings
    print(len('spam'))
    print('---')

    # Lists
    lst = ['a', 'b', 1]
    print(len(lst))
    print('---')

    # Tuples
    tup = (1,2,True,False,"Hi")
    print(len(tup))

    4
    ---
    3
    ---
    5

In [41]:

    # Concatenation

    # Strings
    str1 = 'hello ' + 'world ' + 'helloworld'
    print(str1)
    print('---')

    # Lists
    lst = ['a']
    new_lst =  lst + [4, 5, 6]
    print(new_lst)
    print('---')

    # Tuples
    print((1, 2) + (3, 4))

    hello world helloworld
    ---
    ['a', 4, 5, 6]
    ---
    (1, 2, 3, 4)

In [42]:

    # Repetition

    # Strings
    t = 'hi' * 4
    print(t)
    print('---')

    # Lists
    new_lst = lst * 2
    print(new_lst)
    print('---')

    # Tuples
    tup = (1,2)
    new_tup = tup * 4
    print(new_tup)

    hihihihi
    ---
    ['a', 'a']
    ---
    (1, 2, 1, 2, 1, 2, 1, 2)

In [43]:

    # Containment

    # Strings
    print('M' in 'MPCS')
    print('T' in 'MPCS')
    print('CS' in 'CS') # Substring containment example
    print("---")

    # Lists
    print(4 in lst)        # 4 is not in the list
    print ('a' in lst)     # 'a' is iniside the list
    print('---')

    # Tuples
    tup = (1,2,True,False,"Hi")
    print("Bob" in tup)
    print("2" in tup)
    print(1 in tup)

    True
    False
    True
    ---
    False
    True
    ---
    False
    False
    True

#### Sequence Indexing¶

Sequence types provides the indexing notation (`S[i]`) to fetch a single item from the a sequence value.

In [47]:

    # Sequence Indexing is zero-based (i.e., the first item in the sequence is at index 0)

    # Strings
    str1 = 'spam'
    print(str1[0])
    print(str1[2])
    print('---')

    # Lists
    lst = ['a', 'b', 1]
    print(lst[0])
    print(lst[2])

    # Since Lists are mutable we can use indexing to update the elements.
    # Remember you cannot due the following with strings or tuples because they are immutable!
    # If you try to update immutable sequences then an error will be raised.
    print(lst)  # Before update ['a','b', '1']
    lst[0] = 3
    lst[1] = 5
    print(lst)  # After updating [3,5,1]
    print('---')

    # Tuples
    tup = (1,2,True,False,"Hi")
    print(tup[3])
    print(tup[2])

    s
    a
    ---
    a
    1
    ['a', 'b', 1]
    [3, 5, 1]
    ---
    False
    True

In [48]:

    # Python allows for "backwards indexing". Placing a "-"
    # before the integer index will force indexing to begin from the end
    # of a sequence.

    # Strings
    str1 = 'spam'
    print(str1[-1]) # 'm' is the last character (-1)
    print(str1[-2]) # 'a' is the second to the last character (-2)
    print('---')

    # Lists
    lst = ['Bob', ['a','b','c'], 5, 6]
    print(lst[-1])  # 6 is the last element in the list (-1)
    print(lst[-3])  # ['a','b','c'] is the third to the last element (-3)

    # Since Lists are mutable we can use indexing to update the elements.
    # Remember you cannot due the following with strings or tuples because they are immutable!
    # If you try to update immutable sequences then an error will be raised.
    lst[-1] = "Hello"
    lst[-3] = "World"
    print(lst)
    print('---')

    # Tuples
    tup = (1,2,True,False,"Hi")
    print(tup[-1]) # "Hi" is the last element in the list (-1)
    print(tup[-2]) # False is the second to the last element (-2)

    m
    a
    ---
    6
    ['a', 'b', 'c']
    ['Bob', 'World', 5, 'Hello']
    ---
    Hi
    False

#### Sequence Slicing¶

Slicing is a from of sequence indexing, where it returns an entire section (i.e. a copy) of a sequence.

- Syntax(`S[i:j]`):
  - `S` is a sequence value
  - the lower bound (`i`) is inclusive (defaults to zero if not specified)
  - the upper bound (`j`) is nonclusive (defaults to `len(S)` if not specified)

Note: There is an extended version of sequence slicing. We will talk about that next week.

In [49]:

    S = 'MPCS51042'
    lst = ['Bob', 'Sally', 'Tina']
    tup = (1,2,3,4,5)

In [51]:

    # Fetches items at offsets 1 up to but not including 3
    print(S[1:3])
    print('---')
    print(lst[1:3])
    print('---')
    print(tup[1:3])

    PC
    ---
    ['Sally', 'Tina']
    ---
    (2, 3)

In [52]:

    # Fetches items at offset 1 through the end (the sequence length)
    print(S[1:])
    print('---')
    print(lst[1:])
    print('---')
    print(tup[1:3])

    PCS51042
    ---
    ['Sally', 'Tina']
    ---
    (2, 3)

In [53]:

    # Fetches items at offset 0 up to but not including 3
    print(S[:3])
    print('---')
    print(lst[:3])
    print('---')
    print(tup[:3])

    MPC
    ---
    ['Bob', 'Sally', 'Tina']
    ---
    (1, 2, 3)

In [54]:

    # Fetches items at offset 0 up to but not including the last item
    print(S[:-1])
    print('---')
    print(lst[:-1])
    print('---')
    print(tup[:-1])

    MPCS5104
    ---
    ['Bob', 'Sally']
    ---
    (1, 2, 3, 4)

In [55]:

    # Creates a copy of S (i.e, [0, len(S)])
    print(S[:])
    print('---')
    print(lst[:])
    print('---')
    print(tup[:])

    MPCS51042
    ---
    ['Bob', 'Sally', 'Tina']
    ---
    (1, 2, 3, 4, 5)

#### String methods¶

Python provides collection of methods (i.e., functions that act on values) that create new strings based on the behavior of the method.

Syntax: `str_value.method_name(arg1,arg2,...)`

- `str_value` - a string value or variable

- `.method_name` - the notation to call a specific string `method_name`. For example,

  - `upper` : capitalizes all the letters of the string.
  - `lower` : lower cases all the letters of the string.
  - `split` : returns a list of words in a string where default separator is any whitespace.
  - `find` : returns the lowest index of a particular substring in a string. If the substring is not found, -1 is returned.
  - `replace` : Replace all occurences of a substring with a new substring.

  - `strip` : returns a copy of the string with the leading and trailing characters removed. Default character to be removed is whitespace.

  - More string methods : https://docs.python.org/3/library/stdtypes.html#string-methods

- `(arg1,arg2,...)` - the arguments for that method.

In [56]:

    spam = "spammy"
    # Replace all mm with xx in S
    new_spam = spam.replace('mm', 'xx')
    print(new_spam)
    print(spam) # Remember old string is not modified. Strings are immutable

    spaxxy
    spammy

In [47]:

    spam = '----SPAM----SPAM----'
    #Search for the index of the first occurence of "SPAM"
    where = spam.find('SPAM')
    print(where)

    4

In [59]:

    words = 'abc edf g'
    # Creates a list of strings. The words are deliminted by default via a whitespace
    lst = words.split()
    print(lst)
    print('---')
    words = "abc,efg,g"
    lst = words.split(",") # Can specify the deliminter
    print(lst)

    ['abc', 'edf', 'g']
    ---
    ['abc', 'efg', 'g']

In [60]:

    # If possible, you can convert any value into a str value by using the "str()" conversion function.
    str1 = str(1.234)
    str2 = str("True")
    str3 = str([1,2,3])
    print(str1)
    print(str2)
    print(str3)
    print(type(str1))
    print(type(str2))
    print(type(str3))

    1.234
    True
    [1, 2, 3]
    <class 'str'>
    <class 'str'>
    <class 'str'>

#### Mutable Sequences Methods/Operators¶

Below are some common methods associated with mutable sequences. All of these methods change the sequence in-place.

-- https://docs.python.org/3/library/stdtypes. html#mutable-sequence-types

In [50]:

    lst = [1,2,3,4]
    lst.append("hi")
    lst.append(True)
    print(lst)

    [1, 2, 3, 4, 'hi', True]

In [61]:

    lst.pop()  # Removes last element
    print(lst)

    ['abc', 'efg']

#### Aside: Range Sequence Type¶

There is one more important immutable sequence type to discuss `range`. The `range` type is an immutable sequence of numbers and is commonly used for looping a specific number of times in `for` loops.

Syntax: `range(stop)` or `range(start,stop)` or `range(start,stop,step)`:

- `start`: The value of the start parameter (or 0 if the parameter was not supplied) inclusive
- `stop` : The value of the stop parameter exclusive
- `step` : The value of the step parameter (or 1 if the parameter was not supplied)

- The `range` type implementes all common sequence operatrions except for concatenation and repetition
- The main advantage of the `range` type is that it always take the same (small) amount of memory, no matter the size of the range it represents.

In [62]:

    r = range(6) # Contains an ordered collection of the values: 0,1,2,3,4,5
    print(r[0])
    print(r[len(r) - 1]) # Last value in the range
    print('----')

    r = range(1,4) # Contains an ordered collection of the values: 1,2,3
    print(r[0])
    print(r[-1])
    print('----')

    # Printing the range value does not show you the contents because the values are generated when needed
    print(r)
    # You can convert it into a list to see the values
    print(list(r))

    0
    5
    ----
    1
    3
    ----
    range(1, 4)
    [1, 2, 3]

## Video 1.4: Control-Flow and Modules¶

### Video Outline¶

1. If-statements
2. Loop statements: for, while loops
3. Function basics
4. Module Basics

#### Control-Flow¶

Python uses the usual flow control statements known from other languages with some caveats

#### Indentation Rules¶

- Unlike many other languages (C,C++,Java), Python does not use braces `{}` to designate code blocks.

- Python uses indentation to signify code block boundaries.

- Statements indented to the same distance belong to the same block of code.

- Blocks end:

  - When the end of file is detected.
  - When a lesser-indented line is detected.

- Examples of indented statements: `for`, `while`, `if`

- Deeply nested blocks are further indented to the right

#### If Statments¶

The `if` statement allows for selecting from alternative actions based on conditional expression results.

Syntax:

    if condition:
        statement1
        statement2
    elif condition2:   #Else if clause, if the first condition fails then we try the next else if clause
        statement3
    else:
        statement4

Notes:

- There needs to be a colon (`:`) after the end of the condition (or the `else` keyword) to signal the beginning of the clause block
- Both the `elif` and `else` statements are optional.
- Parentheses around the conditional expressions are optional.
- Lines below the `if`, `elif`, or `else` must be indented over 4 spaces.

In [64]:

    bmi = 31

    #Determine the BMI category
    if bmi < 18.5:
        print("Underweight")
    elif bmi >= 18.5 and bmi < 24.9:
        print("Normal weight")
    elif bmi >= 24.9 and bmi < 29.9:
        print("Overweight")
    else:
        print("Obese")

    Obese

#### For statements¶

The `for` statement is a looping statement that steps through items in any sequence or iterable object (more on this later...)

Syntax:

For every iteration of the below `for` loop, it assigns the items in the sequence (or iterable object) to `item` (one by one) and then executes the loop body.

    for item in sequence:
        statement1          # Statement1 is executed every iteration
        if cond_exp:
           break            # Immediately goes to statement4
        if cond_exp:
           continue         # Immediately goes to the next iteration
        statements2         # Statement2 is executed if the continue or break was not encountered.
    else:
       statement3           # Statement3 is executed if loop exits without a break

    statements4             # Statement4 is executed no matter how the loop exts

In [65]:

    bmis = [12,23.45,17.56,32.34,5.6]

    for bmi in bmis:
        category = ''
        #Determine the BMI category
        if bmi < 18.5:
            category = "Underweight"
        elif bmi >= 18.5 and bmi < 24.9:
            category = "Normal weight"
        elif bmi >= 24.9 and bmi < 29.9:
            category = "Overweight"
        else:
            category = "Obese"

        output = f'BMI = {bmi}, Category = {category}'
        print(output)
    else:
        print("Processed all bmis successfully")

    BMI = 12, Category = Underweight
    BMI = 23.45, Category = Normal weight
    BMI = 17.56, Category = Underweight
    BMI = 32.34, Category = Obese
    BMI = 5.6, Category = Underweight
    Processed all bmis successfully

In [66]:

    # Useful For loop Idioms

    # Iterating over a range of numbers using range
    for num in range(10):
        print(num)

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9

In [68]:

    # Iterating over first 20 even numbers
    for even_num in range(2,21,2): # 21 because the stop is exclusive
        print(even_num)

    2
    4
    6
    8
    10
    12
    14
    16
    18
    20

In [69]:

    # Iterating over a collection of items and their indicies
    # Call the "enumerate" function on the sequence
    bmis = [12,23.45,17.56,32.34,5.6]

    for index,element in enumerate(bmis):
        print(f'Index = {index}, BMI = {element}')

    Index = 0, BMI = 12
    Index = 1, BMI = 23.45
    Index = 2, BMI = 17.56
    Index = 3, BMI = 32.34
    Index = 4, BMI = 5.6

#### While Statement¶

A `while` loop repeatedly executes a block of statements as long as its condition expression evaluates to a `True` value.

    while condition:
        statement1          # Statement1 is executed every iteration
        if cond_exp:
           break            # Immediately goes to statement4
        if cond_exp:
           continue         # Immediately goes to the next iteration
        statements2         # Statement2 is executed if the continue or break was not encountered.
    else:
       statement3           # Statement3 is executed if loop exits without a break

    statements4             # Statement4 is executed no matter how the loop exts

In [70]:

    bmis = [23.45,18.67,32.34,5.6]
    index = 0
    # Keep looping while all values are not underweight
    while True:
        bmi = bmis[index]
        index = index + 1
        if bmi < 18.5:
            break
        elif bmi >= 24.9:
            continue

        #Only printing normal weight bmis
        print(f'BMI = {bmi}, Category = Normal Weight')

    BMI = 23.45, Category = Normal Weight
    BMI = 18.67, Category = Normal Weight

#### Function Basics¶

A function is a set of statements that can be called more than once.

Benefits of functions:

- Encapsulation: package logic for use in multiple places
- Allows programmer to avoid copy/paste to repeat same task, which helps maximize code reuse and minimize redundancy
- Procedural decomposition: split our program into subtasks (i.e., functions) with separate roles.
- Make life easier for debugging, testing, doing maintenance on code

#### Function Syntax¶

    def name(arg1, arg2, ..., argN):
         '''
             Description of function task

             Inputs:
                 arg1(type): description of arg1
                 arg2(type): description of arg2
                 ...
                 argN(type): description of argN

             Outputs:
                 Description of what this function returns

        '''
        statement1
        statement2
        ...
        return value

The following are the steps to define a function:

1. The `def` defines a new function called `name`.
2. Define zero or more parameters separated by commas in parentheses
3. A colon to indicate the beginning of the function body.
4. Every function must define a docstring: A docstring is a string literal that provides a description of the function, input arguments and output result.
5. Statements inside function are indented over and don’t run until the function is called
6. `return` can appear anywhere in body of function
   - Can also be omitted. A function that does not return a value will return `None` by default.

In [71]:

    def bmi_category(bmi):
        '''
        Calculates the bmi category given a bmi value

        Inputs:
           bmi(float): the bmi value to use

        Output:
           Returns the bmi category for the bmi value.

        '''
        category = ""
        if bmi < 18.5:
            category = "Underweight"
        elif bmi >= 18.5 and bmi < 24.9:
            category = "Normal weight"
        elif bmi >= 24.9 and bmi < 29.9:
            category = "Overweight"
        else:
            category = "Obese"

        return category

    # Call the function by specifying its name followed by any necessary arguments to the function
    print(bmi_category(17.34))
    print(bmi_category(24.53))

    Underweight
    Normal weight

#### Pass statement¶

The `pass` statement useful when you are not ready to write the implementation for a code block. The statement allows the interpreter to perform no operation and to continue processing the next statement.

In [60]:

    def bmi_category(bmi):
        '''
        Calculates the bmi category given a bmi value

        Inputs:
           bmi(float): the bmi value to use

        Output:
           Returns the bmi category for the bmi value.

        '''
        pass # IMPLEMENT ME

    print("----begin processing bmi values---")
    print(bmi_category(17.34)) # No error, prints None because we do not have a return statement so None is returned
    print(bmi_category(24.53)) # No error, prints None because we do not have a return statement so None is returned
    print("Done.")

    ----begin processing bmi values---
    None
    None
    Done.

In [72]:

    # Pass can be used in any code block statements: loops, classes (later), functions, etc.

    print("Start")
    for r in range(10):
        pass
    print("Done")

    Start
    Done

#### Module Basics¶

As stated earlier, Python programs can be defined in `.py` files. These files are also known as modules. A module is a file containing Python definitions and statements. The file name is the module name minus the `.py` suffix.

What if you want these files to interact with each other (i.e., call functions, use variables from different modules)?

- Modules import other modules by using the `import` statement.

       - ``import module_name``
       -  Import statements should be placed at the beginning of a python file.

- Once imported, you can access the definitions and statements within the module.

       - default way to access variabls, and statements is to use dot notation, where you specify the name of the module followed by a period, and then the code you want to access.
        - ``module_name.variable`` or ``module_name.func(arg1,arg2,...)``

Python provides an extensive amount of predefined modules as part of its standard library. We will look at the `math` module and see how we can import and use the code within that module.

In [62]:

    # 1st import the math module
    import math

    print(math.pi)          # Print the constant PI from the math module.
    print(math.fabs(-232))  # Call the floating point abs function from the math module

    3.141592653589793
    232.0

In the following weeks, we will learn more about modules and ways to use them efficiently.