Functions
An Expected Failure
We have a grade.py file here that’s meant to read a command-line argument that represents a percentage grade and then it will print out the letter grade that the given percentage represents.
Right now our program doesn’t print anything at all. See if you can figure out how to get a letter grade from the given percentage.
import sys
def percent_to_grade(percent):
if percent >= 90:
return 'A'
elif percent >= 80:
return 'B'
elif percent >= 70:
return 'C'
elif percent >= 60:
return 'D'
else:
return 'F'
percent = sys.argv[1]
Another Expected Failure
Copy-paste this code into a file called dollars.py:
import sys
def main():
try:
[amount] = sys.argv[1:]
except ValueError:
sys.exit("Usage: {sys.argv[0]} AMOUNT\nAMOUNT is a number in USD")
print(format_usd(amount))
if __name__ == "__main__":
main()
This program is meant to read a command-line argument that represents an amount of money in US dollars and then print out the dollar amount with a $ before it and with two numbers after the decimal point.
The program is broken because it calls a format_usd function that doesn’t exist yet.
See if you can write the needed format_usd function.
Hint
>>> number = 4.2
>>> text = f"{number:.02f}"
>>> text
'4.20'
What’s a function?
A function is a group of commands. Functions can take inputs (called arguments) and can provide an output (a return value). We can “call” a function to run the commands in it.
We’ve already seen the len function:
>>> name = "Trey"
>>> len(name)
4
What other functions have we seen?
Defining
We can define functions in Python like this:
>>> def greet(name):
... print(f"Hello {name}")
...
The name of the function is greet, and name is the input parameter or argument to the function.
We can call (execute) our function like this:
>>> greet("Trey")
Hello Trey
>>> greet("world")
Hello world
Defining a function in a module
Let’s make a function for our greetings module.
Change your greetings.py script to look like this:
import sys
def greet(name):
print(f"Hello {name}!")
if __name__ == "__main__":
greet(sys.argv[1])
Now we can run our program like this:
$ python3 greetings.py Trey
Hello Trey!
We can also import our greet function and use it from the REPL or another module:
>>> from greetings import greet
>>> greet("Trey")
Hello Trey!
Return
Functions in Python can also have return values.
Return values are the output of a function.
We can use a return statement to return something from our function:
>>> def multiply(x, y):
... return x * y
...
>>> multiply(7, 3)
21
Note that 21 is only printed out above because we’re at the Python REPL.
If we capture the return value of that multiply function call into a variable, we won’t see that return value printed out (unless we specifically look at the value of the variable):
>>> z = multiply(7, 3)
>>> z
21
None: The Default Return Value
What happens when a function has no return statement?
So far, our functions always printed something if it did not return anything.
In the REPL, this often looks the same.
Here are two functions, double_print and double_return.
One returns the answer and the other prints the answer.
>>> def double_return(number):
... return(number * 2)
...
>>> def double_print(number):
... print(number * 2)
...
>>> double_return(12)
24
>>> double_print(12)
24
They look exactly the same! How can you tell, in the REPL, if the function returns or prints? You can print the result of the function call:
>>> print(double_return(12))
24
>>> print(double_print(12))
24
None
What is that None doing there?
When a function does not return anything in the code, Python gives it a return value of None.
The REPL does not display anything when something has a return value of None.
But in our example of double_print, we told Python to print the return value of the function, so after the function printed the value, it returned None, which is printed for us.
None Represents Emptiness
Sometimes you want to represent purposeful emptiness, and this is where None is useful.
Let’s say we want to represent that we have a name variable representing the user’s name but we don’t actually have the user’s name yet.
We could do this:
>>> name = ""
>>> name
''
Are there any problems with this?
Problem: how can we tell whether the user entered a blank (empty) name or whether they were never even asked for a name?
Some languages have a concept of “null” or “undefined”. In Python we call this None:
>>> name = None
>>> name
>>>
None represents nothingness. If you think of an empty string or the number 0 as being like air, then None is like a vacuum.
None is “falsey”, meaning is evaluates to False:
>>> name = None
>>> if name:
... print("there is a name")
...
None has a special NoneType:
>>> type(None)
<class 'NoneType'>
Note
The is operator can be used to determine whether two things are identical objects (they point to the same object in memory) instead of just equal. This is sometimes used to determine whether a value is None:
>>> meaning = None
>>> if meaning is None:
... print("there is no meaning")
...
there is no meaning
The is operator is often used when comparing singletons. None, True, and False are all singletons. That means there is exactly one copy of each of these objects floating around in your Python program.
You can also use is to check for equality to True or False but this is often considered poor form in Python because relying on truthiness is typically preferred:
>>> meaning = "be good"
>>> if meaning is True:
... print("meaning is true")
... elif meaning:
... print("meaning is truthy")
...
meaning is truthy
Keyword Arguments
Python also allows you to specify arguments by their name when calling functions. When arguments are specified this way they are called “keyword arguments”:
Let’s try this with our greet function:
>>> from greetings import greet
>>> greet(name="Trey")
Hello Trey!
>>> greet("Trey")
Hello Trey!
If we have multiple arguments specified as keywords, the order doesn’t matter:
>>> def divide(x, y):
... return x / y
...
>>> divide(12, 4)
3.0
>>> divide(x=12, y=4)
3.0
>>> divide(y=4, x=12)
3.0
So Python has two ways of specifying arguments. You can pass positional arguments to a function (that’s the usual style that most programming languages have) but you can also pass keyword arguments (a.k.a named arguments).
Can you supply positional arguments before keyword arguments when calling a function?
Can you supply keyword arguments before positional arguments when calling a function?
What happens if you supply the same keyword argument twice?
What happens if you supply an argument both positionally and by its name?
Default Arguments
We can also specify default values for arguments when defining our functions. This makes an argument optional, so when the function is called, if no argument is supplied, it gets the default value:
>>> def greet(name="world"):
... print(f"Hello {name}!")
...
>>> greet()
Hello world!
>>> greet("Trey")
Hello Trey!
>>> greet(name="Trey")
Hello Trey!
This can be extremely useful for functions that can have a number of inputs, but where, most of the time, they would always be the same. By supplying the default values, a programmer using the function would not have to worry about supplying these arguments all the time.
Note
While it’s a good code practice to put spaces around the = operator in assignment statements, this is not the case for keyword arguments and default arguments.
It is considered a best practice not to put spaces around the equal sign when specifying keyword arguments and default arguments.
Tip
About Importing: When Python imports something in the REPL, it reads and evaluates it immediately and then caches the imported code. Even if you tell it to import it again, Python sees that it imported it once already, and ignores the import command. This is an intentional design of the software so that there isn’t a problem with circular imports. It also makes the code faster; for example, if multiple modules all import module A, module A is only imported once.
Function Exercises
Hint
If you get stuck for a minute or more, try searching Google or using help.
If you’re stuck for more than a few minutes, some of these links might be helpful for some of the exercises below:
Hypotenuse
File: Edit the get_hypotenuse function in the file functions.py that is in the exercises directory.
Test: Run python test.py get_hypotenuse in your exercises directory.
Exercise: Edit the function get_hypotenuse so that it returns the hypotenuse of a right triangle given the other two sides. To test the function in the REPL, you can paste the function in from your text editor or import it as shown:
>>> from functions import get_hypotenuse
>>> get_hypotenuse(3, 4)
5.0
>>> get_hypotenuse(5, 12)
13.0
Hint: to get the square root of a number, raise it to the power of 0.5.
Note that if you change the code in the file after you import it, then to test the new code, you must exit the REPL, restart it and import the function again.
Note
Want to test this out manually (instead of using python test.py)?
You could could create a file called get_hypotenuse_test.py with your own test code:
from functions import get_hypotenuse
print("Calling get_hypotenuse(3, 4)")
print("Expected: 5.0")
print(" Actual:", get_hypotenuse(3, 4))
print()
print("Calling get_hypotenuse(5, 12)")
print("Expected: 13.0")
print(" Actual:", get_hypotenuse(5, 12))
Then you can run that file to test your code:
$ python get_hypotenuse_test.py
Parse Time
File: Edit the parse_time function in the file functions.py that is in the exercises directory.
Test: Run python test.py parse_time in your exercises directory.
Exercise: Edit the function parse_time so that it takes an input time string in the format of “minutes:seconds”.
The function extracts the minutes and seconds, calculates and returns the total number of seconds.
You can assume the string will contain at least one digit each for minutes and seconds.
>>> from functions import parse_time
>>> parse_time("4:03")
243
>>> parse_time("0:12")
12
>>> parse_time("1:10")
70
Note that if you change the code in the file after you import it, then to test the new code, you must exit the REPL, restart it and import the function again.
Format Time
File: Edit the format_time function in the file functions.py that is in the exercises directory.
Test: Run python test.py format_time in your exercises directory.
Exercise: Edit the function format_time so that it takes an integer value of seconds and calculates minutes and seconds, then returns a string of “minutes:seconds”. If the value of seconds is less than 10, a zero should be prefixed so the seconds are always 2 digits.
Hint
Some helpful hints if you need them:
>>> from functions import format_time
>>> format_time(90061)
'1501:01'
>>> format_time(0)
'0:00'
>>> format_time(3600)
'60:00'
>>> format_time(301)
'5:01'
>>> format_time(3715)
'61:55'
>>> format_time(61)
'1:01'
>>> format_time(119)
'1:59'
>>> format_time(333)
'5:33'
Coalesce
File: Edit the coalesce function in the file functions.py that is in the exercises directory.
Test: Run python test.py coalesce in your exercises directory.
Exercise: Edit the function coalesce so that it takes a value and a default; returning default if value is None and returning value if it is not None.
This is similar to the SQL coalesce operation.
There was actually an idea to add a ?? coalesce operator to Python 3.8; that idea has been deferred.
To test the function in the REPL, you can paste the function in from your text editor or import it as shown:
>>> from functions import coalesce
>>> name = "Trey"
>>> coalesce(name, "")
'Trey'
>>> name = None
>>> coalesce(name, "")
''
Note that if you change the code in the file after you import it, then to test the new code, you must exit the REPL, restart it and import the function again.
To Percent
File: Edit the to_percent function in the file functions.py that is in the exercises directory.
Test: Run python test.py to_percent in your exercises directory.
Exercise: Edit the function to_percent so that it takes a number and returns a string of the percentage representation of the value with one decimal place.
To test the function in the REPL, you can paste the function in from your text editor or import it as shown:
>>> from functions import to_percent
>>> to_percent(0.02534)
'2.5%'
>>> to_percent(1.5678)
'156.8%'
Note that if you change the code in the file after you import it, then to test the new code, you must exit the REPL, restart it and import the function again.
To Celsius
File: Edit the to_celsius function in the file functions.py that is in the exercises directory.
Test: Run python test.py to_celsius in your exercises directory.
Exercise: Edit the function to_celsius that accepts a temperature in Fahrenheit as input and returns a temperature in Celsius. To test the function in the REPL, you can paste the function in from your text editor or import it as shown:
>>> from functions import to_celsius
>>> to_celsius(212)
100.0
Is Leap Year
File: Edit the is_leap_year function in the file functions.py that is in the exercises directory.
Test: Run python test.py is_leap_year in your exercises directory.
Exercise: Edit the function is_leap_year so that it takes a year as input and returns True if and only if the given year is a leap year.
Leap years are years that are divisible by 4
Exception: centennials (years divisible by 100) are not leap years
Exception to exception: years divisible by 400 are leap years
You may find this pseudocode from Wikipedia helpful.
>>> from functions import is_leap_year
>>> is_leap_year(1900)
False
>>> is_leap_year(2000)
True
>>> is_leap_year(2012)
True
>>> is_leap_year(2018)
False
Perfect Square
File: Edit the is_perfect_square function in the file functions.py that is in the exercises directory.
Test: Run python test.py is_perfect_square in your exercises directory.
Exercise: Edit the function is_perfect_square so that it returns True if the given number is a perfect square.
A perfect square is a number which is the square of an integer. For example 25 (5 * 5), 49 (7 * 7) and 81 (9 * 9) are all perfect squares.
>>> from functions import is_perfect_square
>>> is_perfect_square(119025)
True
>>> is_perfect_square(81)
True
>>> is_perfect_square(33)
False
>>> is_perfect_square(1024)
True
>>> is_perfect_square(24)
False