7. Functions#

Most of the Python code you have seen and written in these notebooks has been small fragments. In some cases, these fragments can produce interesting and useful results, such as computing compound interest. However, it does not make sense to copy and paste these fragments into other programs constantly. Fortunately, Python provides a mechanism to organize these code blocks into a cohesive unit - the function. These notebooks have used many built-in Python functions (int, float, print, input, type, help, dir); now, we will develop custom functions in this notebook.

Functions are named reusable code blocks. They can take any number of parameters to send data into the function. Functions can then return any number of results to the calling block. Functions exist to perform a specific task within the construct of a larger program.

As you use functions within your programs, two fundamental concepts exist:

  1. Defining (declaring) the function. This provides the function’s definition: the name, parameters, and functionality. In many other programming languages, you need to state the functionʼs return type and parameter types explicitly.

  2. Calling the function. As we call (execute) a function, we pass a specific list of arguments as the values for a function’s parameters.

7.1. Defining#

We declare a function with the keyword def, followed by its name, zero or more parameters specified within parenthesis, a colon, and then an indented code block. The syntax -

def function_name(parameter):
    block

Calling a user-defined function is the same as calling a built-in function - use the name followed by a parenthesis enclosing any arguments.

1def say_hello():
2    print("Hello World!")
3    
4say_hello()
Hello World!
1def say_greeting(name):
2    print("Hello",name,"- it is nice to meet you")
3    
4say_greeting("Steve")
Hello Steve - it is nice to meet you

The first line of a function is called the function header, which specficies the name of the name of the function and any parameters to to the function. The function header starts with the keyword def and ends with the colon. The portion following is the header is known as the block, which is the set of statements executed when the function is called.

If a function lists one or more parameters in its definition, you must pass the required number of corresponding arguments. Not passing a required argument causes a “TypeError”.

1say_greeting()      # raises a TypeError
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[3], line 1
----> 1 say_greeting()      # raises a TypeError

TypeError: say_greeting() missing 1 required positional argument: 'name'

7.2. Arguments vs. Parameters#

Colloquially, programmers use the terms arguments and parameters interchangeably. However, a slight distinction exists between the two. Parameters are the variables defined for use within a function as part of its declaration. Arguments are the values passed to a particular function when a statement calls that function.

In the above example, “name” is a parameter and “Steve” is an argument.

7.3. Function naming rules#

Function names follow the same rules as variables:

  • can only contain lowercase letters (a-z), uppercase letters (A-Z), digits (0-9), and an underscore(_)

  • cannot begin with a digit

  • cannot be one of Python’s reserved keywords.

Function names are case-sensitive.

Just as with variable names, you should create meaningful names that communicate the purpose of the function.

Python’s naming conventions use underscores separating words versus using camelCase present in other languages like Java.

7.4. Returning Values#

Use the return keyword followed by the appropriate value or variable to return a value from a function.

The Python interpreter implicitly returns None if the function exits (reaches the bottom of the function) without a return value.

The None keyword defines a null value (or no value at all). None is not the same as 0, False, or an empty string. None has a type of “NoneType”.

1def add_numbers(a, b):
2    return a + b
3
4print(add_numbers(5, 6))
5print(add_numbers(1968, 2001))
11
3969
1print (False == None)   # None is not the same thing as False
2x = None
3print (x is None)       # test if x is the object "None"
4print (x is not None)
5print(type(None))
False
True
False
<class 'NoneType'>
1def print_add_numbers(a, b):
2    print(a + b)
3    
4x = print_add_numbers(4,5)
5print(type(x))
6if not x:
7    print("None evaluates to False in a conditional check.")
9
<class 'NoneType'>
None evaluates to False in a conditional check.

In print_add_numbers, no return statement exists, so None is implicitly returned.

7.5. Parameters#

Python has several conveniences when defining parameters and passing arguments.

First, we can match arguments to parameters when they are called simply by their order:

1def print_message(owner, color, thing):
2    print(owner,"has a",color, thing)
3    
4print_message("John","grey","car")
5print_message("Pat", "light green","car")
John has a grey car
Pat has a light green car

Second, we can match arguments to parameters by the names of the corresponding parameter. These are keyword arguments.

1print_message(thing="phone",owner="Joseph", color="red")
Joseph has a red phone

Third, we can mix and match positional and keyword arguments. Any positional arguments must come first.

1print_message("Joseph", thing="phone", color="red")
Joseph has a red phone
1print_message(thing="phone", color="red","Joseph")   # Raises a syntax error.
  Cell In[10], line 1
    print_message(thing="phone", color="red","Joseph")   # Raises a syntax error.
                                                     ^
SyntaxError: positional argument follows keyword argument

Fourth, we can specify default parameter values:

1def print_message(owner, color='black', thing='phone'):
2    print(owner,"has a",color, thing)
3    
4print_message('John')
5print_message('John', thing="car")
6print_message('John', 'red','pen')
John has a black phone
John has a black car
John has a red pen

Now, practice making different calls to the following function. How many different paths are there to get an answer?

 1def play_golf(outlook="overcast", windy=False, humidity="normal"):
 2    if outlook == "rainy":
 3        if windy:
 4            return False
 5        else:
 6            return True
 7    elif outlook == "overcast":
 8        return True
 9    elif outlook == "sunny":
10        if humidity == "high":
11            return False
12        elif humidity == "normal":
13            return True
14        else:
15            print("Undefined humidity: "+humidity)
16    else:
17        print("Undefined outlook: "+outlook)
1print(play_golf())
2print(play_golf("sunny"))
True
True

We can represent the logic in the above method as a decision tree:

By counting the number of leaf nodes (those with an oval / no children), we get the number of different paths. This number is the minimum number of test cases needed to test the function.

Complete the decision matrix: (Fill in the missing blanks.)

outlook

windy

humidity

decision

true

rainy

true

rainy

false

sunny

true

sunny

false

Finally, Python has two capabilities to handle situations where the number of parameters is unknown. A * can be used to gather requirements into a list, while ** can be used to collect pairs of arguments into a dictionary. We will explore both of these possibilities in later notebooks.

7.6. Local Variables#

We’ve seen how parameters function similarly to variables in the previous code examples. In addition to using those parameters, we can also define new variables within functions. A variable defined within a function is local variable and can only be accessed(used) within that function. Other functions may define local variables with the same name, but those variables are unique to the enclosing function. Further, once a function returns a local variable can no longer be accessed.

In this example, we define a function to square a particular number. In that function, we create a local variable result to temporary store the computation. The main code then calls square and a local variable result is is initialized with the result of 5*5 (25). The function then returns the result. The main code then prints out the return value from square. In the last line, we demonstrate the error that occurs when we try to access result.

1def square(a):
2    result = a * a
3    return result
4
5print("Square of 5:",square(5))
6print(result)      # raises a NameError as result is not visible outside of the function square
Square of 5: 25
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[14], line 6
      3     return result
      5 print("Square of 5:",square(5))
----> 6 print(result)      # raises a NameError as result is not visible outside of the function square

NameError: name 'result' is not defined

7.7. Docstrings and Providing Function Documentation#

We can provide documentation for a function by using a docstring at the start of a function. Docstrings are strings contained right after the function definition line. (They can also be used at the start of module and class definitions as well.)

With docstrings, other programmers can see information on your function, including an overall description, any parameters, and the return value. As with using whitespace, comments, and informative variable names, docstrings are a fundamental part to making programs readable.

Documentation Strings

The interpreter assigns the literal to a special attribute, __doc__ of that object. IDEs, other programming environments, and Python’s help() function use the contents of these docstrings to provide information to their users.

The following code block defines a new function with a docstring. We then call the function, call the help() function, and then print the value of the __doc__ attribute.

 1def calculate_mortgage_payment(principal, interest_rate):
 2    """Provides a simple mortgage calculator to compute the monthly payment for a 30-year fixed mortgage"""
 3    
 4    rate = interest_rate / 12
 5    num_payments = 360
 6    payment = principal * ( rate * (1+rate)**num_payments ) / ( (1+rate)**num_payments -1)
 7    return payment
 8
 9print(calculate_mortgage_payment(100000,.05))
10print("-------------------------")
11help(calculate_mortgage_payment)
12print("-------------------------")
13print(calculate_mortgage_payment.__doc__)
536.8216230121398
-------------------------
Help on function calculate_mortgage_payment in module __main__:

calculate_mortgage_payment(principal, interest_rate)
    Provides a simple mortgage calculator to compute the monthly payment for a 30-year fixed mortgage

-------------------------
Provides a simple mortgage calculator to compute the monthly payment for a 30-year fixed mortgage

Internally, Python treats defined functions as objects. Everything is an object in Python - even functions. Everything has some properties (state) and some methods/functions (behavior).

The interpreter automatically assigns function docstrings to the property __doc__.

The single-line docstring shown above suffices for simple or self-obvious methods. However, for more complicated situations, we should provide additional details. For this class, we will use Google Style DocStrings.

 1def play_golf(outlook="overcast", windy=False, humidity="normal"):
 2    """
 3    play_golf determines whether or not a golfer should play 
 4    golf based on the current weather conditions.
 5    
 6    Args:
 7       outlook (str): Is the weather going to be "sunny", "overcast", or "rainy"?
 8       windy (bool): Is it going to be windy?  True/False
 9       humidity (str): Is the humidity "high" or "normal"
10
11    Returns:
12    True if the individual should play golf.  False if not
13    """
14    
15    if outlook == "rainy":
16        if windy:
17            return False
18        else:
19            return True
20    elif outlook == "overcast":
21        return True
22    elif outlook == "sunny":
23        if humidity == "high":
24            return False
25        elif humidity == "normal":
26            return True
27        else:
28            print("Undefined humidity: "+humidity)
29    else:
30        print("Undefined outlook: "+outlook)
31        return False
1help (play_golf)
Help on function play_golf in module __main__:

play_golf(outlook='overcast', windy=False, humidity='normal')
    play_golf determines whether or not a golfer should play 
    golf based on the current weather conditions.
    
    Args:
       outlook (str): Is the weather going to be "sunny", "overcast", or "rainy"?
       windy (bool): Is it going to be windy?  True/False
       humidity (str): Is the humidity "high" or "normal"
    
    Returns:
    True if the individual should play golf.  False if not

7.8. Namespaces and Scope#

As you declare functions and use variables, you create identifiers that refer to those objects (functions, parameters, and variables). A namespace is a dictionary of symbolic names and the objects referred to by those names. For example, “play_golf” is a name that refers to a specific function.

Python contains four types of namespaces:

  1. Built-in. Contains the names and references for Python’s built-in objects (which includes functions!).

  2. Global. Contains any names defined at the main level of a program. The Python interpreter automatically creates this namespace when it starts. The variables we create outside of functions, and the functions we have created so far belong to this namespace.

  3. Enclosing - As later notebooks demonstrate, developers can create functions created within classes and within other functions. In these situations, the interpreter creates an enclosing namespace. We also demonstrate this with the “The LEGB Rule” section below.

  4. Local - The interpreter creates this namespace when a function is called. When the function exits, the interpreter destroys the corresponding namespace.

The scope of a name is the region of the program in which it is valid. Revisiting the building analogy from the third notebook, just like buildings can be in different neighborhoods or areas of a city, variables can have different scopes in programming. Some variables are local to a specific function or block of code, while others are global and can be accessed from anywhere in the program.

Consider the following example:

 1a = 10 
 2
 3def f(param1): 
 4    a = 20
 5    b = 15
 6    print("1:",param1)
 7    print("2:", a)
 8    
 9def g():
10    print("3:",a)
11
12f(5)
13g()
14print("4:",a)
15print("5:",b)      # raises a name error
1: 5
2: 20
3: 10
4: 10
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[18], line 15
     13 g()
     14 print("4:",a)
---> 15 print("5:",b)      # raises a name error

NameError: name 'b' is not defined

Execute this code on PythonTutor

Code Explanation:

  • Line 1: Defines a variable a in the global namespace.

  • Line 3: Defines a function f in the global namespace.

  • Line 9: Defines a function g in the global namespace.

  • Line 12: Calls the function f with the argument 5. Creates a local namespace, adds param1 to that namespace

    • Line 4: defines a new variable a in the local names space, value = 20

    • Line 5: defines a new variable b in the local names space, value = 5

    • Line 8: f returns None as there was not an explicit return. Local namespace destroyed

  • Line 13: Calls the function g. Creates a local namespace

    • Line 10: As a does not belong to the local namespace, the interpreter searches the enclosing namespaces for the variable. Found in the global namespace, the interpreter prints 10.

    • Line 11: g returns None as there was not an explicit return. Local namespace destroyed.

  • Line 14: prints 10 from the variable stored in the global namespace.

  • Line 15: raises a “NameError” as b is undefined.

The use of a in the two functions might be a little confusing. No special keywords are needed if we access a variable within an enclosing namespace. If we define a variable with the same name, we create a local variable.

Use the above link to step through this code line by line to help visualize what occurs.

In the following example, we use the global keyword to explicitly access and use a global variable within a function.

 1a = 10 
 2
 3def f(param1): 
 4    global a
 5    a = 20
 6    print("1:",param1)
 7    print("2:", a)
 8
 9f(5)
10print("3:",a)
1: 5
2: 20
3: 20

Execute this code on PythonTutor

If you ever want to look at the contents of a given namespace, you can use:

  • dir(__builtins__)

  • globals()

  • locals()

Try out each of these in the cell below:

1dir(__builtins__)
Hide code cell output
['ArithmeticError',
 'AssertionError',
 'AttributeError',
 'BaseException',
 'BaseExceptionGroup',
 'BlockingIOError',
 'BrokenPipeError',
 'BufferError',
 'BytesWarning',
 'ChildProcessError',
 'ConnectionAbortedError',
 'ConnectionError',
 'ConnectionRefusedError',
 'ConnectionResetError',
 'DeprecationWarning',
 'EOFError',
 'Ellipsis',
 'EncodingWarning',
 'EnvironmentError',
 'Exception',
 'ExceptionGroup',
 'False',
 'FileExistsError',
 'FileNotFoundError',
 'FloatingPointError',
 'FutureWarning',
 'GeneratorExit',
 'IOError',
 'ImportError',
 'ImportWarning',
 'IndentationError',
 'IndexError',
 'InterruptedError',
 'IsADirectoryError',
 'KeyError',
 'KeyboardInterrupt',
 'LookupError',
 'MemoryError',
 'ModuleNotFoundError',
 'NameError',
 'None',
 'NotADirectoryError',
 'NotImplemented',
 'NotImplementedError',
 'OSError',
 'OverflowError',
 'PendingDeprecationWarning',
 'PermissionError',
 'ProcessLookupError',
 'RecursionError',
 'ReferenceError',
 'ResourceWarning',
 'RuntimeError',
 'RuntimeWarning',
 'StopAsyncIteration',
 'StopIteration',
 'SyntaxError',
 'SyntaxWarning',
 'SystemError',
 'SystemExit',
 'TabError',
 'TimeoutError',
 'True',
 'TypeError',
 'UnboundLocalError',
 'UnicodeDecodeError',
 'UnicodeEncodeError',
 'UnicodeError',
 'UnicodeTranslateError',
 'UnicodeWarning',
 'UserWarning',
 'ValueError',
 'Warning',
 'ZeroDivisionError',
 '__IPYTHON__',
 '__build_class__',
 '__debug__',
 '__doc__',
 '__import__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'abs',
 'aiter',
 'all',
 'anext',
 'any',
 'ascii',
 'bin',
 'bool',
 'breakpoint',
 'bytearray',
 'bytes',
 'callable',
 'chr',
 'classmethod',
 'compile',
 'complex',
 'copyright',
 'credits',
 'delattr',
 'dict',
 'dir',
 'display',
 'divmod',
 'enumerate',
 'eval',
 'exec',
 'execfile',
 'filter',
 'float',
 'format',
 'frozenset',
 'get_ipython',
 'getattr',
 'globals',
 'hasattr',
 'hash',
 'help',
 'hex',
 'id',
 'input',
 'int',
 'isinstance',
 'issubclass',
 'iter',
 'len',
 'license',
 'list',
 'locals',
 'map',
 'max',
 'memoryview',
 'min',
 'next',
 'object',
 'oct',
 'open',
 'ord',
 'pow',
 'print',
 'property',
 'range',
 'repr',
 'reversed',
 'round',
 'runfile',
 'set',
 'setattr',
 'slice',
 'sorted',
 'staticmethod',
 'str',
 'sum',
 'super',
 'tuple',
 'type',
 'vars',
 'zip']

7.9. The LEGB Rule#

While not explicitly mentioned in the Python literature, the LEGB Rule is a mnemonic for how the Python interpreter searches the various namespaces for a particular item. As you can see from the diagram above with how namespaces enclose each other, the interpreter searches the namespaces in the following order:

  1. Local

  2. Enclosing

  3. Global

  4. Built-in

In the following code block, we create an enclosing function g within f to demonstrate an enclosing namespace.

 1a = 10 
 2
 3def f(param1):
 4    x = 5               # x is local to f
 5    y = 3               # y is local to f
 6    
 7    def g(param2):
 8        z = 15          # z is local to g
 9        print("3:",x)   # access x from the enclosing namespace
10        nonlocal y      # tell the Python interpreter to give access to modify y
11        y = 100         # modify y in the enclosing namespace.  Rerun the this block and delete the previous
12                        # line to see how the output changes for "5:" changes as y becomes local
13        print("4:",y)
14        
15    global a            # gives access to the global variable a. Comment/delete this line to make local.
16    a = 20              # modify the global variable a.  
17    print("1:",param1)
18    print("2:", a)
19    g(10)
20    print("5:",y)
21
22f(5)
23print("6:",a)     # access a global
1: 5
2: 20
3: 5
4: 100
5: 100
6: 20

7.10. Passing Arguments: By Object Reference#

As everything is an object in Python, the language passes a reference to an object when that object is an argument to a function. From a performance perspective, making complete copies of complex or large objects would be costly.

If we pass a mutable object to a function, that function could change the object’s properties (e.g., add an element to a list). As we write functions, we need to avoid unintended side effects on objects passed to us or manipulate global variables.

In this next code block, the built-in function id() returns the identity of an object. This identity is a unique integer. When val is assigned 12, a new object is created and assigned to that parameter. a was unchanged as an integer is an immutable type - instances of immutable types have state (values) which cannot be modified once the instance (object) has been created.

 1a = 1024
 2print("Line 2:",id(a))
 3
 4def changeImmutableParameter(val):
 5    print("Line 5: function called with "+str(val))
 6    print("Line 6:", id(val))
 7    val = 512
 8    print("Line 8:", id(val))
 9    print("Line 9: val is now " + str(val))
10
11changeImmutableParameter(a)
12print("Line 12:", a)
13print("Line 13:", id(a))
Line 2: 4404409584
Line 5: function called with 1024
Line 6: 4404409584
Line 8: 4404408432
Line 9: val is now 512
Line 12: 1024
Line 13: 4404409584

If you search the Internet, you will see several different justifications as to whether or not Python uses “call by value” or “call by reference”.

“Call by value” evaluates the arguments and passes their actual values to the called function.[1]

In “call by reference”, the caller passes a reference (pointer) to the storage address of the actual parameter.[1] A pointer is variable that contains an explicit memory address. As discussed in a previous notebook, a reference is a value that points to an object stored in the computer’s memory. Unlike pointers, we cannot directly access the value of a reference nor change the value of a reference.

In some other languages, programmers can also pass the address of the actual variable to the function, giving the function an alias to that location. Python (nor Java) does not provide this capability.

Others will say that Python is “call by value” when passing immutable objects (integers, floats, strings, tuples, etc.) because any changes made to those parameters within the function are not reflected in the calling function. Then they will say for mutable objects (lists, dictionaries,etc.), that Python is “call by reference”. These statements are fundamental misunderstandings and incorrect interpretations. In both cases, the reference to the object has been passed.

(This digression may seem overly fastidious, but interviewers often ask how different languages pass parameters. The advice here is not to answer value/reference but rather to explain how Python passes arguments by sending object references.)

Note: As the id() function returns the unique identifer of an object, we can again equate this to our building analogy from the third notebook in that this identifier is similar to the address of a building in which both uniquely identify “something”.

7.11. Case Study: Revisiting the System Investment Calculator#

Now, revisit the process for creating a systematic investment plan (SIP) calculator as presented in a prior notebook. A SIP is an investment product offered by many mutual fund companies. Investors put a fixed amount of money into the SIP periodically (e.g., monthly) to help promote financial discipline and provide dollar cost averaging.

\( FV = P \times \dfrac{(1+i)^n - 1}{i} \times (1 +i) \)

  • \(FV\) is the future value

  • \(P\) is the amount invested at the start of every payment interval

  • \(i\) is the periodic interest rate; for a monthly payment, take the (Annual Rate)/12

  • \(n\) is the number of payments

Now, create a function to compute this value. The function needs to have three arguments - \(P\), \(i\), and \(n\). The function then returns the future value of the investment.

1def compute_sip_future_value(payment, periodic_interest_rate, num_payments):
2    return payment * \
3           ((1 + periodic_interest_rate)**num_payments - 1)/periodic_interest_rate *\
4           (1 + periodic_interest_rate)
1# invest $100 monthly for 30 years at 10% interest
2compute_sip_future_value(100, .10/12,30*12)
227932.53241693613

Notes:

  1. The function only computes and returns the future value. The function has no responsibility to get the input parameters or display the results. With computer programming / software development, this is known as the single-responsibility principle.

  2. By placing this capability into a function, we now provide an easy-to-use abstraction of computing future values for ourselves. We no longer have to concern ourselves with the exact details and steps of the process.

7.12. Case Study: Mortgage Affordability Calculator#

Suppose as part of building a comprehension personal finance site, you were asked to create a mortgage affordability calculator. The goal of this calculator is to compute the maximum value an individual has to purchase a home. We’ll use the following conditions:

  • Borrowers may only use up 28% of their gross income for housing payments

  • The default annual percentage rate is 3%. However, this rate is only available to borrows with a perfect credit score of 850. For borrowers who have less than perfect score, they pay an additional basis point (0.01%) for each point their score is under 850 (provided their credit score is greater than or equal to 700). For borrowers, who have a credit score between 600 and 699, they pay 2 basis points (0.02%) for each point they are under 850. Borrow who have a credit score between 500 and 599, pay 3 basis points (0.03%) for each point they are under 850. Borrowers with a credit score less than 500 and ineligible for mortgage.

  • The housing payment includes homeowners insurance and property tax. Homeowners insurance can be assumed to be \(\$1,000\) and the property tax \(\$5,000\) (both annually).

  • Homeowners can provide a down payment.

As you look at those conditions, you should see that we need these input values: annual gross income, credit score, down payment.

Our output is the maximum home purchase price.

Programmers often apply a design approach make to decompose a large task into smaller, more easily performed tasks. This approach also supports testing and reusability. As we look at this problem, we have a number of different tasks:

  • input annual gross income

  • input credit score

  • input down payment

  • determine loan eligibility

  • determine interest

  • compute initial loan (principal)

  • output maximum home purchase price

Below shows how we can organize this information into an IPO chart:

Mortgage Affordability: Maximum Home Price
Input Processing Output
annual gross income
credit score
down payment
  • input annual gross income
  • input credit score
  • input down payment
  • determine loan eligibility
  • determine interest
  • compute max payment
  • compute initial loan (principal)
  • comput maximum home price
  • output maximum home purchase price
maximum home purchase price

Now that we’ve formed an initial design for this process, we begin to look at how we could implement this into code. One way is simple to put this into a single block of code. However, this way, while simplistic and straightforward, has a number of issues. First, testing the code becomes increasing more difficult versus smaller, more singular focused code blocks. Making logic changes such as taking into account varying homeowners insurance takes is more difficult as it is harder to determine the impacts of changes. Additional coding the entire process as a single unit makes reusing any portion of the process more difficult - we would need to copy and paste code, which leads to maintainability issues when changes have to be made in the future.

The following functions show the individual functions that we can create for each of the processing steps. Ideally, each of the input functions should have additional logic to validate user input.

 1def input_annual_gross_income(): 
 2    return int(input("Enter your annual gross income:"))
 3
 4def input_credit_score():
 5    return int(input("Enter your credit score (0-850)"))
 6
 7def input_down_payment():
 8    return int(input("Enter the amount of money you have available for a down payment:"))
 9
10def is_eligible_for_loan(credit_score):
11    return credit_score >= 500
12
13def compute_annual_percentage_rate(credit_score, base_apr):
14    # should check that the credit score is between 500 and 850
15    if credit_score >= 700:
16        return (850 - credit_score) * 0.0001 + base_apr
17    elif credit_score >= 600:
18        return (850 - credit_score) * 0.0002 + base_apr
19    else:
20        return (850 - credit_score) * 0.0003 + base_apr
21
22def compute_max_payment(annual_gross_income):
23    result = annual_gross_income * .28 / 12  # max monthly payment is 28% of income
24    result = result - 1000/12                # substract homeowners insurance
25    result = result - 5000/12                # substract property tax
26    return result
27
28def compute_principal(payment, terms_per_year, annual_interest_rate, years):
29    result = payment * (1- (1 + annual_interest_rate/terms_per_year)**(-1 * years * terms_per_year))/ ( annual_interest_rate/terms_per_year)
30    return result
31
32def compute_max_home_price(principal, down_payment):
33    return principal + down_payment
34
35def output_max_home_purchase(amount,apr):
36    print("Congratulations, you can afford a house worth $", amount, "with a", apr*100,"% loan")

Now, we create a main function to tie all of these together.

 1def main():
 2    base_apr = 0.03
 3    agi = input_annual_gross_income()
 4    credit_score = input_credit_score()
 5    down_payment = input_down_payment()
 6    if not is_eligible_for_loan(credit_score):
 7        print("Your credit score is too low to qualify for a mortgage")
 8        return
 9    apr = compute_annual_percentage_rate(credit_score, base_apr)
10    max_payment = compute_max_payment(agi)
11    if (max_payment <= 0):
12        if agi < 6000:
13            print("You do not make enough to pay homeowners insurance and property taxes")
14            return
15        max_payment = 0
16    loan_principal = compute_principal(max_payment,12, apr, 30)
17    max_home_price = compute_max_home_price(loan_principal, down_payment)
18    output_max_home_purchase(max_home_price,apr) 
1main()
Enter your annual gross income: 85000
Enter your credit score (0-850) 715
Enter the amount of money you have available for a down payment: 20000
Congratulations, you can afford a house worth $ 317970.71002268925 with a 4.35 % loan

Formula for the present value (initial principal of a loan): \(P_{0}=\frac{d\left(1-\left(1+\frac{r}{k}\right)^{-Nk}\right)}{\left(\frac{r}{k}\right)}\) where

  • \(P_0\) is the amount of the loan

  • \(d\) loan payment

  • \(r\) annual interest rate (as a decimal)

  • \(k\) number of compounding periods

  • \(N\) length of the loan in years

7.13. Global Variables#

Generally speaking, global variables should not be used. By having global variables, it becomes very difficult to track where changes occur and often leads to unintended side effects with functions. Additionally, if the global variable can be read and written to by multiple threads, an inappropriate system state may be created where one thread has read an older or partially defined object while another thread is in the process of changing it.

Using global variables as constant value may be an appropriate use case, but generally we should place those within modules (which will be discussed later).

7.14. Suggested LLM Prompts#

  • Explain the concept of functions in Python, their purpose, and the benefits of using them, such as code reusability, modularization, and abstraction. Provide examples of simple functions and their use cases.

  • Discuss the syntax of defining functions in Python, including the use of the def keyword, function names, parameters, and return statements. Walk through the process of creating and calling a function, and explain how arguments are passed and processed.

  • Introduce the concept of function signatures, including positional arguments, keyword arguments, and default argument values. Provide examples of how to define and call functions with different argument types, and discuss best practices for choosing appropriate argument styles.

  • Explore the concept of variable scope in Python functions, including global and local variables. Explain how Python handles variable assignments and references within and outside of functions, and discuss best practices for managing variable scope.

  • Introduce the concept of function documentation and docstrings in Python. Explain the importance of documenting functions, and provide examples of how to write clear and concise docstrings using the appropriate formatting and conventions.

7.15. Review Questions#

  1. What is the purpose of a function in Python? Provide an example of when you might use a function.

  2. What are the different parts of a function in Python?

  3. How do you call or invoke a function in Python? Provide an example.

  4. What is the difference between a function parameter and a function argument? Give an example of each.

  5. How does a function send result(s) back to the caller? What statement is used? When should it be used and what occurs? When is that statement optional?

  6. If a function exits in Python without a return, what value does the caller receive (if any)?

  7. Explain the concept of variable scope in Python functions. What is the difference between local and global variables?

  8. What are default parameter values in Python functions? Provide an example of how to define and use them.

  9. What is the purpose of a docstring in a Python function? How do you write a docstring, and what are the conventions?

  10. What is the difference between printing a value and returning a value?

answers

7.16. Exercises#

  1. Sleep Time? Complete the following function -

def sleep_in(weekday, vacation):
    """
    Returns True if we are on vacation or if it is not a weekday - that is we can sleep late.
    Otherwise, False is returned.  Both arguments are Boolean.
    """

Original Source: https://codingbat.com/prob/p173401

  1. Sum or Multiply - Complete the following function:

def sum_or_multiply(a,b):
    """
    if a and b are the same number, return their sum.
    Otherwise return the value of multiplying a by b.
    """

Adapted from: https://codingbat.com/prob/p141905

  1. Near Value? Complete the following function -

def near_value(number, value, margin):
    """
    Checks if the parameter number is close to the parameter value by being within the given margin.
    near_value(97,100,5) returns True
    near_value(110,100,5) returns False

    Args:
       number (int/float): what number to check to see if its close to value
       value (int/float): check that the number is within margin of the value (+/-)
       margin (int/float): how close does the number need to be

    Returns:
    True if number is within margin of the given value.
    """

Note: abs(num) computes the absolute value of a number. Adapted from: https://codingbat.com/prob/p124676

  1. Using the formula for compound interest, write a function named compute_compound_interest to compute and return the result. The function should take three parameters (in this order): the initial principal, the interest rate, and the number of years. Assume the interest is compounded monthly.

  2. Repeat exercise 4, but add an optional parameter that determines the number of periods annually. The default should be 12.

  3. Using the formula to compute monthly payments for a loan, write a function named compute_monthly_payments to compute those payment amounts. The function should take the following parameters: principal, interest_rate, and number_of_payments.

    \( A = P \times \dfrac{r(1+r)^n}{(1+r)^n - 1} \)

  • \(A\) is the periodic amortization payment

  • \(P\) is the principal amount borrowed

  • \(r\) is the rate of interest expressed as a fraction; for a monthly payment, take the (Annual Rate)/12

  • \(n\) is the number of payments; for monthly payments over 30 years, 12 months x 30 years = 360 payments.

  1. While the standard unit of measurement for mass is the kilogram, many different units exist for measuring mass (and weight). Create a function called convert_mass with three parameters: value - the number for the current mass, current - the unit currently used for the mass, and target - the unit to convert the mass for the result. The function should return the number for the current mass expressed in the target unit. Use the following conversion tables for the possible current and target units.

Unit Name

Kilograms

Kilogram

1.0

Pound

0.453592

Stone

6.35029

Jin

0.5

Seer

1.25

Gram

0.001

Oka

1.2829

convert_mass(10,"Jin","Pound") returns approximately 11.023

7.17. References#

[1] Alfred V. Aho, Ravi Sethi, and Jeffrey D. Ullman. 1986. Compilers: principles, techniques, and tools. Addison-Wesley Longman Publishing Co., Inc., USA.