Takeaways from Python Crash Course: Python Function
This post is a record made while learning Chapter 8 “Functions” in Eric Matthes’s book, Python Crash Course.1
Python function terms
1
2
3
4
5
def greet_user():
"""Display a simple greeting."""
print("Hello!")
greet_user()
1
Hello!
(1) def
in def greet_user():
is a Python keyword.
(2) def greet_user():
is the function definition, where greet_user()
is the function name. This function needs no information to receive, so there is nothing in its parentheses (even so, the parentheses are required). Finally, the definition ends in a colon.
If forget parentheses, we’ll get an error:
1
2
3
4
5
def greet_user:
"""Display a simple greeting."""
print("Hello!")
greet_user()
1
2
3
4
Cell In[1], line 1
def greet_user:
^
SyntaxError: expected '('
(3) Any indented lines that follow def greet_user():
make up the body of the function.
(4) The text """Display a simple greeting."""
enclosed in triple quotes is a comment called docstring, which describes what the function does.2
(5) greet_user()
in the 5th line is a function call. A function call tells Python to execute the code in the function. To call a function, we should write the function name, followed by any necessary information in parentheses. There is no information needed in this case, so we call the function simply by entering greet_user()
.
Here is a case of passing information to a function:
1
2
3
4
5
def greet_user(username):
"""Display a simple greeting."""
print(f"Hello, {username.title()}!")
greet_user('jesse')
1
Hello, Jesse!
The variable username
in the function definition (1st line) is a parameter, and the value 'jesse'
in the function call (5th line) is an argument. An argument is a piece of information that’s passed from a function call to a function.
Pass arguments
Positional arguments
When we call a function, Python must match each argument in the function call with a parameter in the function definition. The simplest way to do this is based on the order of the arguments provided. Values matched up in this way are called positional arguments. Definitely, arguments order matters in this case.
1
2
3
4
5
6
def describe_pet(animal_type, pet_name):
"""Display information about a pet."""
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet('hamster', 'harry')
1
2
I have a hamster.
My hamster's name is Harry.
Keyword arguments
A keyword argument is a name-value pair that we pass to a function. Keyword arguments free us from worrying about having to worry about correctly ordering arguments in the function call because Python knows where each value should go.
1
2
3
4
5
6
def describe_pet(animal_type, pet_name):
"""Display information about a pet."""
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet(pet_name='harry', animal_type='hamster')
1
2
I have a hamster.
My hamster's name is Harry.
Specify default values
In the function definition, we can specify a default value for each parameter. Using default values can simplify function calls and clarify the ways in which the functions are typically used.
1
2
3
4
5
6
7
def describe_pet(pet_name, animal_type='dog'):
"""Display information about a pet."""
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet('willie')
describe_pet(pet_name='willie')
1
2
3
4
5
I have a dog.
My dog's name is Willie.
I have a dog.
My dog's name is Willie.
It should be noted that when use default values, any parameter with a default value needs to be listed after all the parameters that don’t have default values. This allows Python to continue interpreting positional arguments correctly, otherwise an error will occur, e.g.
1
2
3
4
def describe_pet(animal_type='dog', pet_name):
"""Display information about a pet."""
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
1
2
3
4
Cell In[6], line 1
def describe_pet(animal_type='dog', pet_name):
^
SyntaxError: non-default argument follows default argument
Make an argument optional (1)
Sometimes it makes sense to make an argument optional so that people using the function can choose to provide extra information only if they want to. We can use default values to make an argument optional.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def get_formatted_name(first_name, last_name, middle_name=''):
"""Return a full name, neatly formatted."""
if middle_name:
full_name = f"{first_name} {middle_name} {last_name}"
else:
full_name = f"{first_name} {last_name}"
return full_name.title()
musician = get_formatted_name('jimi', 'hendrix')
print(musician)
musician = get_formatted_name('john', 'hooker', 'lee')
print(musician)
1
2
Jimi Hendrix
John Lee Hooker
As mentioned before, we have to make sure the middle name is the last argument passed (because the middle name is optional with default value an empty string) so that Python could match up the positional arguments correctly.
Make an argument optional (2)
1
2
3
4
5
6
7
8
9
10
11
12
def build_person(first_name, last_name, age=None):
"""Return a dictionary of information about a person."""
person = {'first': first_name, 'last': last_name}
if age:
person['age'] = age
return person
musician = build_person('jimi', 'hendrix')
print(musician)
musician = build_person('jimi', 'hendrix', age=27)
print(musician)
1
2
{'first': 'jimi', 'last': 'hendrix'}
{'first': 'jimi', 'last': 'hendrix', 'age': 27}
Here a new optional parameter age
is added to the function definition and assigned the special value None
, which is used when a variable has no specific value assigned to it. We can think of None
as a placeholder value.
Pass a list
When we pass a list to a function, the function gets direct access to the contents of the list.
1
2
3
4
5
6
7
8
def greet_users(names):
"""Print a simple greeting to each user in the list."""
for name in names:
msg = f"Hello, {name.title()}!"
print(msg)
usernames = ['hannah', 'ty', 'margot']
greet_users(usernames)
1
2
3
Hello, Hannah!
Hello, Ty!
Hello, Margot!
Modify a list in a function
We can modify a list by passing it to a defined function, and any changes made to the list inside the function’s body are permanent, like print_models()
function in the following example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def print_models(unprinted_designs, completed_models):
"""
Simulate printing each design, until none are left.
Move each design to completed_models after printing.
"""
while unprinted_designs:
current_design = unprinted_designs.pop()
print(f"Printing model: {current_design}")
completed_models.append(current_design)
def show_completed_models(completed_models):
"""Show all the models that were printed."""
print("\nThe following models have been printed:")
for completed_model in completed_models:
print(completed_model)
unprinted_designs = ['phone case', 'robot pendant', 'dodecahedron']
completed_models = []
print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)
# Display uncompleted models
print("\nThe following models have not been printed:")
print(unprinted_designs)
1
2
3
4
5
6
7
8
9
10
11
Printing model: dodecahedron
Printing model: robot pendant
Printing model: phone case
The following models have been printed:
dodecahedron
robot pendant
phone case
The following models have not been printed:
[]
This example also demonstrates the idea that every function should have one specific job. This is more beneficial than using one function to do all jobs.
Prevent a function from modifying a list
We could preserve the original contents of a list by passing a copy of the list to the function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def print_models(unprinted_designs, completed_models):
"""
Simulate printing each design, until none are left.
Move each design to completed_models after printing.
"""
while unprinted_designs:
current_design = unprinted_designs.pop()
print(f"Printing model: {current_design}")
completed_models.append(current_design)
def show_completed_models(completed_models):
"""Show all the models that were printed."""
print("\nThe following models have been printed:")
for completed_model in completed_models:
print(completed_model)
unprinted_designs = ['phone case', 'robot pendant', 'dodecahedron']
completed_models = []
print_models(unprinted_designs[:], completed_models)
show_completed_models(completed_models)
# The original list of unprinted designs will be unaffected by the function
print(f"\n{unprinted_designs}")
1
2
3
4
5
6
7
8
9
10
Printing model: dodecahedron
Printing model: robot pendant
Printing model: phone case
The following models have been printed:
dodecahedron
robot pendant
phone case
['phone case', 'robot pendant', 'dodecahedron']
However, it’s not recommended unless we have a specific reason. It’s more efficient for a function to work with an existing list to avoid using the time and memory needed to make a separate copy, especially when we’re working with large lists.
Pass arbitrary number of arguments
Arbitrary number of arguments: *args
1
2
3
4
5
6
def make_pizza(*toppings):
"""Print the list of toppings that have been requested."""
print(toppings)
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')
1
2
('pepperoni',)
('mushrooms', 'green peppers', 'extra cheese')
The asterisk in the parameter name *toppings
tells Python to make an empty tuple called toppings
and pack whatever values it receives into this tuple. Above results show that Python can handle a function call with one value and a call with three values. It treats different calls similarly. Note that Python packs the arguments into a tuple, even if the function only receives one value.
Besides, if we want a function to accept several different kinds of arguments (like mix positional arguments and arbitrary arguments for example), the parameter that accepts an arbitrary number of arguments must be placed last in the function definition. Python matches positional and keyword arguments first and then collects any remaining arguments in the final parameter.
1
2
3
4
5
6
7
8
def make_pizza(size, *toppings):
"""Summarize the pizza we are about to make."""
print(f"\nMaking a {size}-inch pizza with the following toppings:")
for topping in toppings:
print(f"- {topping}")
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
1
2
3
4
5
6
7
Making a 16-inch pizza with the following toppings:
- pepperoni
Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
Generally, generic parameter name *args
is more often used (rather than specific name, *toppings
in this case), which means collecting arbitrary positional arguments like this.
Arbitrary number of keyword arguments: **kwargs
Sometimes we want to accept an arbitrary number of arguments, but we don’t know what kind of information will be passed to the function in advance. In this case, we can write functions that accept as many key-value pairs as the calling statement provides.
1
2
3
4
5
6
7
8
9
10
def build_profile(first, last, **user_info):
"""Build a dictionary containing everything we know about a user."""
user_info['first_name'] = first
user_info['last_name'] = last
return user_info
user_profile = build_profile('albert', 'einstein',
location='princeton',
field='physics')
print(user_profile)
1
{'location': 'princeton', 'field': 'physics', 'first_name': 'albert', 'last_name': 'einstein'}
The double asterisks in the parameter **user_info
inform Python to create an empty dictionary called user_info
and pack whatever name-value pairs it receives into this dictionary. Within the function, we can access the key-value pairs in user_info
as other ordinary dictionaries.
Similarly, generic parameter name **kwargs
is generally used, stating to collect non-specific keyword arguments.
Mix *args
and **kwargs
Using *args
and **kwargs
simultaneously when defining a function is really a common case, e.g.
1
2
3
4
5
6
7
def helper_show_info(name, *args, **kwargs):
print(name)
print(args)
print(kwargs)
helper_show_info('Tommy', 'man', '18',
mobile='12345678900', address='Birmingham')
1
2
3
Tommy
('man', '18')
{'mobile': '12345678900', 'address': 'Birmingham'}
And, we should also note the order, including the order in the function definition (*args
followed by **kwargs
):
1
2
3
4
5
6
7
def helper_show_info(name, **kwargs, *args):
print(name)
print(args)
print(kwargs)
helper_show_info('Tommy', 'man', '18',
mobile='12345678900', address='Birmingham')
1
2
3
4
Cell In[4], line 1
def helper_show_info(name, **kwargs, *args):
^
SyntaxError: arguments cannot follow var-keyword argument
and the order in the function call:
1
2
3
4
5
6
7
8
def helper_show_info(name, *args, **kwargs):
print(name)
print(args)
print(kwargs)
helper_show_info('Tommy',
mobile='12345678900', address='Birmingham',
'man', '18')
1
2
3
4
Cell In[6], line 8
'man', '18')
^
SyntaxError: positional argument follows keyword argument
Store functions in the module
We can store all self-defined functions in a separate file (called a module) and then import the module into the main script file by the import
statement.
For instance, one function named make_pizza()
is defined and put in the module pizza.py
:
1
2
3
4
5
def make_pizza(size, *toppings):
"""Summarize the pizza we are about to make."""
print(f"\nMaking a {size}-inch pizza with the following toppings:")
for topping in toppings:
print(f"- {topping}")
Here are several ways to import the module and functions within it.
Import the entire module: import module_name
1
2
3
4
import pizza
pizza.make_pizza(16, 'pepperoni')
pizza.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
1
2
3
4
5
6
7
Making a 16-inch pizza with the following toppings:
- pepperoni
Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
The line import pizza
tells Python to open the file pizza.py
and copy all the functions from it into the current script file before running the subsequent code.
Import specific functions: from module_name import function_0, function_1, function_2
1
2
3
from pizza import make_pizza
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
1
2
3
4
5
6
7
Making a 16-inch pizza with the following toppings:
- pepperoni
Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
Give a function an alias: from module_name import function_name as fn
1
2
3
4
from pizza import make_pizza as mp
mp(16, 'pepperoni')
mp(12, 'mushrooms', 'green peppers', 'extra cheese')
1
2
3
4
5
6
7
Making a 16-inch pizza with the following toppings:
- pepperoni
Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
This method is useful when the name of imported function conflicts with an existing name in the current file, or the case that function name is long.
Give a module an alias: import module_name as mn
1
2
3
4
import pizza as p
p.make_pizza(16, 'pepperoni')
p.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
1
2
3
4
5
6
7
Making a 16-inch pizza with the following toppings:
- pepperoni
Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
Calling the function by p.make_pizza()
is not only more concise than pizza.make_pizza()
, but also redirects our attention from the module name and allows us to focus on the descriptive names of its functions. This way is more readable.
Import all functions in a module: from module_name import *
We can tell Python to import all functions in a module by asterisk operator *
:
1
2
3
4
from pizza import *
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
1
2
3
4
5
6
7
Making a 16-inch pizza with the following toppings:
- pepperoni
Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
Due to that every function in pizza
module is imported, we can call each function by name without dot notation. However, this method is not that recommended, because we may get some unexpected results if function names conflict.
Style functions and modules
There are several conventions when styling functions and modules.
-
Functions and modules should have descriptive names, which can help others to understand what the function and module are trying to do.
-
Function and module names should be composed of lowercase letters and underscores.
-
Every function should have a comment that explains concisely what the function does. This comment should appear immediately after the function definition and use the docstring format. In a well-documented function, other programmers can use the function by reading only the description in the docstring.
-
When specifying a default value for a parameter, no spaces should be used on either side of the equal sign:
1
def function_name(parameter_0, parameter_1='default value')
and the same convention is used for keyword arguments in function calls:
1
function_name(value_0, parameter_1='value')
-
PEP 8 recommends that programmers limit lines of code to 79 characters so every line is visible in a reasonably sized editor window3. If a set of parameters causes a function definition longer than 79 characters, press
[Enter]
after the opening parenthesis on the definition line. On the next line, press[Tab]
twice to separate the list of arguments from the function body, which will only be indented one level.1 2 3 4 5
def function_name( parameter_0, parameter_1, parameter_2, parameter_3, parameter_4, parameter_5): # function body ... pass
-
If a module has more than one function, we can separate each by two blank lines to make it easier to see where one function ends and the next one begins.
-
All
import
statements should be written at the beginning of a file. The only exception is if we write comments at the beginning of the file to describe the overall script.
References
-
Python Crash Course: A Hands-on, Project-based Introduction to Programming (Second Edition), Eric Matthes, pp. 129-156. ˄
-
Explore Python Module, Class, and Function by
help
Function,__doc__
Attribute, anddir
Function - WHAT A STARRY NIGHT~. ˄ -
PEP 8: Maximum Line Length – Style Guide for Python Code | peps.python.org. ˄