DEV Community

Ahmed Gouda
Ahmed Gouda

Posted on • Originally published at ahmedgouda.hashnode.dev

Python Functions

Functions, which are named blocks of code that are designed to do one specific job.

When you want to perform a particular task that you’ve defined in a function, you call the function responsible for it. If you need to perform that task multiple times throughout your program, you don’t need to type all the code for the same task again and again; you just call the function dedicated to handling that task, and the call tells Python to run the code inside the function. You’ll find that using functions makes your programs easier to write, read, test, and fix.

Defining a Function

Here’s a simple function named greet_user() that prints a greeting:

def greet_user():
    """Display a simple greeting."""
    print("Hello!")

greet_user()
Enter fullscreen mode Exit fullscreen mode
Hello!
Enter fullscreen mode Exit fullscreen mode

This example shows the simplest structure of a function.

  • keyword def informs Python that you’re defining a function. This is the function definition, which tells Python the name of the function and, if applicable, what kind of information the function needs to do its job.

  • The parentheses hold that information. In this case, the name of the function is greet_user(), and it needs no information to do its job, so its parentheses are empty.

  • Finally, the definition ends in a colon.

Any indented lines that follow def greet_user(): make up the body of the function. The text """Display a simple greeting.""" is a comment called a docstring, which describes what the function does. Docstrings are enclosed in triple quotes, which Python looks for when it generates documentation for the functions in your programs.

The line print("Hello!") is the only line of actual code in the body of this function, so greet_user() has just one job: print("Hello!").

When you want to use this function, you call it. A function call tells Python to execute the code in the function. To call a function, you write the name of the function, followed by any necessary information in parentheses.

Passing Information to a Function

Modified slightly, the function greet_user() can not only tell the user Hello! but also greet them by name. For the function to do this, you enter username in the parentheses of the function’s definition at def greet_user(). By adding username here you allow the function to accept any value of username you specify. The function now expects you to provide a value for username each time you call it. When you call greet_user(), you can pass it a name, such as 'Mohammed', inside the parentheses:

def greet_user(username):
    """Display a simple greeting."""
    print(f"Hello, {username.title()}!")

greet_user('Mohammed')
Enter fullscreen mode Exit fullscreen mode
Hello, Mohammed!
Enter fullscreen mode Exit fullscreen mode

Entering greet_user('Mohammed') calls greet_user() and gives the function the information it needs to execute the print() call. The function accepts the name you passed it and displays the greeting for that name.

You can call greet_user() as often as you want and pass it any name you want to produce a predictable output every time.

Arguments and Parameters

In the preceding greet_user() function, we defined greet_user() to require a value for the variable username. Once we called the function and gave it the information (a person’s name), it printed the right greeting.

The variable username in the definition of greet_user() is an example of a
parameter
, a piece of information the function needs to do its job. The value 'Mohammed' in greet_user('Mohammed') is an example of an argument. An argument is a piece of information that’s passed from a function call to a function. When we call the function, we place the value we want the function to work
with in parentheses. In this case the argument 'Mohammed' was passed to the function greet_user(), and the value was assigned to the parameter username.

People sometimes speak of arguments and parameters interchangeably. Don’t be surprised if you see the variables in a function definition referred to as arguments or the variables in a function call referred to as parameters.

Passing Arguments

Because a function definition can have multiple parameters, a function call may need multiple arguments. You can pass arguments to your functions in a number of ways. You can use positional arguments, which need to be in the same order the parameters were written; keyword arguments, where each argument consists of a variable name and a value; and lists and dictionaries of values.

Positional Arguments

When you 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 this way are called positional arguments.

To see how this works, consider a function that displays information about pets. The function tells us what kind of animal each pet is and the pet’s name:

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')
Enter fullscreen mode Exit fullscreen mode

I have a hamster.
My hamster's name is Harry.
Enter fullscreen mode Exit fullscreen mode

The definition shows that this function needs a type of animal and the animal’s name. When we call describe_pet(), we need to provide an animal type and a name, in that order.

Multiple Function Calls

You can call a function as many times as needed. Describing a second, different pet requires just one more call to describe_pet():

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')
describe_pet('dog', 'willie')
Enter fullscreen mode Exit fullscreen mode

I have a hamster.
My hamster's name is Harry.

I have a dog.
My dog's name is Willie.
Enter fullscreen mode Exit fullscreen mode

Calling a function multiple times is a very efficient way to work. The code describing a pet is written once in the function. Then, anytime you want to describe a new pet, you call the function with the new pet’s information. Even if the code for describing a pet were to expand to ten lines, you could still describe a new pet in just one line by calling the function again.

You can use as many positional arguments as you need in your functions. Python works through the arguments you provide when calling the function and matches each one with the corresponding parameter in the function’s definition.

Order Matters in Positional Arguments

You can get unexpected results if you mix up the order of the arguments in a function call when using positional arguments:

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('harry', 'hamster')
Enter fullscreen mode Exit fullscreen mode

I have a harry.
My harry's name is Hamster.
Enter fullscreen mode Exit fullscreen mode

In this function call we list the name first and the type of animal second. Because the argument 'harry' is listed first this time, that value is assigned to the parameter animal_type. Likewise, 'hamster' is assigned to pet_name.

Keyword Arguments

A keyword argument is a name-value pair that you pass to a function. You directly associate the name and the value within the argument, so when you pass the argument to the function, there’s no confusion (you won’t end up with a harry named Hamster). Keyword arguments free you from having to worry about correctly ordering your arguments in the function call, and they clarify the role of each value in the function call.

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(animal_type='hamster', pet_name='harry')
Enter fullscreen mode Exit fullscreen mode

I have a hamster.
My hamster's name is Harry.
Enter fullscreen mode Exit fullscreen mode

The function describe_pet() hasn’t changed. But when we call the function, we explicitly tell Python which parameter each argument should be matched with. When Python reads the function call, it knows to assign the argument 'hamster' to the parameter animal_type and the argument 'harry' to pet_name. The output correctly shows that we have a hamster named Harry.

The order of keyword arguments doesn’t matter because Python knows where each value should go. The following two function calls are equivalent:

describe_pet(animal_type='hamster', pet_name='harry')
describe_pet(pet_name='harry', animal_type='hamster')
Enter fullscreen mode Exit fullscreen mode

When you use keyword arguments, be sure to use the exact names of the parameters in the function’s definition.

Default Values

When writing a function, you can define a default value for each parameter. If an argument for a parameter is provided in the function call, Python uses the argument value. If not, it uses the parameter’s default value. So when you define a default value for a parameter, you can exclude the corresponding argument you’d usually write in the function call. Using default values can simplify your function calls and clarify the ways in which your functions are typically used.

For example, if you notice that most of the calls to describe_pet() are being used to describe dogs, you can set the default value of animal_type to 'dog'. Now anyone calling describe_pet() for a dog can omit that information:

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(pet_name='willie')
Enter fullscreen mode Exit fullscreen mode

I have a dog.
My dog's name is Willie.
Enter fullscreen mode Exit fullscreen mode

We changed the definition of describe_pet() to include a default value, 'dog', for animal_type. Now when the function is called with no animal_type specified, Python knows to use the value 'dog' for this parameter.

Note that the order of the parameters in the function definition had to be changed. Because the default value makes it unnecessary to specify a type of animal as an argument, the only argument left in the function call is the pet’s name. Python still interprets this as a positional argument, so if the function is called with just a pet’s name, that argument will match up with the first parameter listed in the function’s definition. This is the reason the first parameter needs to be pet_name.

The simplest way to use this function now is to provide just a dog’s name in the function call describe_pet('willie').

This function call would have the same output as the previous example. The only argument provided is 'willie', so it is matched up with the first parameter in the definition, pet_name. Because no argument is provided for animal_type, Python uses the default value 'dog'.

To describe an animal other than a dog, you could use a function call like this:

describe_pet(pet_name='harry', animal_type='hamster')
Enter fullscreen mode Exit fullscreen mode

Because an explicit argument for animal_type is provided, Python will ignore the parameter’s default value.

When you 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.

Equivalent Function Calls

Because positional arguments, keyword arguments, and default values can all be used together, often you’ll have several equivalent ways to call a function. Consider the following definition for describe_pet() with one default value provided:

def describe_pet(pet_name, animal_type='dog'):
Enter fullscreen mode Exit fullscreen mode

With this definition, an argument always needs to be provided for pet_name, and this value can be provided using the positional or keyword format. If the animal being described is not a dog, an argument for animal_type must be included in the call, and this argument can also be specified using the positional or keyword format.

All of the following calls would work for this function:

# A dog named Willie.
describe_pet('willie')
describe_pet(pet_name='willie')

# A hamster named Harry.
describe_pet('harry', 'hamster')
describe_pet(pet_name='harry', animal_type='hamster')
describe_pet(animal_type='hamster', pet_name='harry')
Enter fullscreen mode Exit fullscreen mode

It doesn’t really matter which calling style you use. As long as your function calls produce the output you want, just use the style you find easiest to understand.

Avoiding Argument Errors

When you start to use functions, don’t be surprised if you encounter errors about unmatched arguments. Unmatched arguments occur when you provide fewer or more arguments than a function needs to do its work. For example, here’s what happens if we try to call describe_pet() with no arguments:

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()
Enter fullscreen mode Exit fullscreen mode
TypeError: describe_pet() missing 2 required positional arguments: 'animal_type' and 'pet_name'
Enter fullscreen mode Exit fullscreen mode

The traceback tells us the call is missing two arguments and reports the names of the missing arguments. If this function were in a separate file, we could probably rewrite the call correctly without having to open that file and read the function code.

Python is helpful in that it reads the function’s code for us and tells us the names of the arguments we need to provide. This is another motivation for giving your variables and functions descriptive names. If you do, Python’s error messages will be more useful to you and anyone else who might use your code.

If you provide too many arguments, you should get a similar traceback that can help you correctly match your function call to the function definition.

Return Values

A function doesn’t always have to display its output directly. Instead, it can process some data and then return a value or set of values. The value the function returns is called a return value. The return statement takes a value from inside a function and sends it back to the line that called the function. Return values allow you to move much of your program’s grunt work into functions, which can simplify the body of your program.

Returning a Simple Value

Let’s look at a function that takes a first and last name, and returns a neatly formatted full name:

def get_formatted_name(first_name, last_name):
    """Return a full name, neatly formatted."""
    full_name = f"{first_name} {last_name}"
    return full_name.title()

boxer = get_formatted_name('mohammed', 'ali')
print(boxer)
Enter fullscreen mode Exit fullscreen mode
Mohammed Ali
Enter fullscreen mode Exit fullscreen mode

When you call a function that returns a value, you need to provide a variable that the return value can be assigned to. In this case, the returned value is assigned to the variable boxer.

This might seem like a lot of work to get a neatly formatted name when we could have just written:

print("Mohammed Ali")
Enter fullscreen mode Exit fullscreen mode

But when you consider working with a large program that needs to store many first and last names separately, functions like get_formatted_name() become very useful. You store first and last names separately and then call this function whenever you want to display a full name.

Making an Argument Optional

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. You can use default values to make an argument optional.

For example, say we want to expand get_formatted_name() to handle middle names as well. A first attempt to include middle names might look like this:

def get_formatted_name(first_name, middle_name, last_name):
    """Return a full name, neatly formatted."""
    full_name = f"{first_name} {middle_name} {last_name}"
    return full_name.title()

user = get_formatted_name('ahmed', 'mohammed', 'gouda')
print(user)
Enter fullscreen mode Exit fullscreen mode
Ahmed Mohammed Gouda
Enter fullscreen mode Exit fullscreen mode

This function works when given a first, middle, and last name. But middle names aren’t always needed, and this function as written would not work if you tried to call it with only a first name and a last name.

To make the middle name optional, we can give the middle_name argument an empty default value and ignore the argument unless the user provides a value. To make get_formatted_name() work without a middle name, we set the default value of middle_name to an empty string and move it to the end of the list of parameters:

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()

user = get_formatted_name('ahmed', 'gouda')
print(user)

user = get_formatted_name('ahmed', 'gouda', 'mohammed')
print(user)
Enter fullscreen mode Exit fullscreen mode
Ahmed Gouda
Ahmed Mohammed Gouda
Enter fullscreen mode Exit fullscreen mode

Optional values allow functions to handle a wide range of use cases while letting function calls remain as simple as possible.

Returning a Dictionary

A function can return any kind of value you need it to, including more complicated data structures like lists and dictionaries. For example, the following function takes in parts of a name and returns a dictionary representing a person:

def build_person(first_name, last_name):
    """Return a dictionary of information about a person."""
    person = {'first': first_name, 'last': last_name}
    return person

boxer = build_person('mohammed', 'ali')
print(boxer)
Enter fullscreen mode Exit fullscreen mode
{'first': 'mohammed', 'last': 'ali'}
Enter fullscreen mode Exit fullscreen mode

The function build_person() takes in a first and last name, and puts these values into a dictionary. The value of first_name is stored with the key 'first', and the value of last_name is stored with the key 'last'. The entire dictionary representing the person is returned. The return value is printed with the original two pieces of textual information now stored in a dictionary.

This function takes in simple textual information and puts it into a more meaningful data structure that lets you work with the information beyond just printing it. The strings 'mohammed' and 'ali' are now labeled as a first name and last name. You can easily extend this function to accept optional values like a middle name, an age, an occupation, or any other information you want to store about a person.

For example, the following change allows you to store a person’s age as well:

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

boxer = build_person('mohammed', 'ali', age=74)
print(boxer)
Enter fullscreen mode Exit fullscreen mode
{'first': 'mohammed', 'last': 'ali', 'age': 74}
Enter fullscreen mode Exit fullscreen mode

We add a new optional parameter age to the function definition and assign the parameter the special value None, which is used when a variable has no specific value assigned to it. You can think of None as a placeholder value. In conditional tests, None evaluates to False. If the function call includes a value for age, that value is stored in the dictionary. This function always stores a person’s name, but it can also be modified to store any other information you want about a person.

Using a Function with a while Loop

We can use the get_formatted_name() function with a while loop to greet users more formally.

def get_formatted_name(first_name, last_name):
    """Return a full name, neatly formatted."""
    full_name = f"{first_name} {last_name}"
    return full_name.title()

while True:
    print("\nPlease tell me your name:")
    print("(enter 'q' at any time to quit)\n")

    f_name = input("First name: ")
    if f_name == 'q':
        break

    l_name = input("Last name: ")
    if l_name == 'q':
        break

    formatted_name = get_formatted_name(f_name, l_name)
    print(f"\nHello, {formatted_name}!")
Enter fullscreen mode Exit fullscreen mode

Please tell me your name:
(enter 'q' at any time to quit)

First name: omar
Last name: ali

Hello, Omar Ali!

Please tell me your name:
(enter 'q' at any time to quit)

First name: ahmed
Last name: gouda   

Hello, Ahmed Gouda!

Please tell me your name:
(enter 'q' at any time to quit)

First name: q
Enter fullscreen mode Exit fullscreen mode

We have to define a quit condition to terminate the while loop. So, we add a message that informs the user how to quit, and then we break out of the loop if the user enters the quit value at either prompt. The program will continue greeting people until someone enters 'q' for either name.

Passing a List

You’ll often find it useful to pass a list to a function, whether it’s a list of names, numbers, or more complex objects, such as dictionaries. When you pass a list to a function, the function gets direct access to the contents of the list. Let’s use functions to make working with lists more efficient.

Say we have a list of users and want to print a greeting to each. The following example sends a list of names to a function called greet_users(), which greets each person in the list individually:

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 = ['mohammed', 'islam', 'ali']
greet_users(usernames)
Enter fullscreen mode Exit fullscreen mode
Hello, Mohammed!
Hello, Islam!
Hello, Ali!
Enter fullscreen mode Exit fullscreen mode

This is the output we wanted. Every user sees a personalized greeting, and you can call the function any time you want to greet a specific set of users.

Modifying a List in a Function

When you pass a list to a function, the function can modify the list. Any changes made to the list inside the function’s body are permanent, allowing you to work efficiently even when you’re dealing with large amounts of data.

Consider a company that creates 3D printed models of designs that users submit. Designs that need to be printed are stored in a list, and after being printed they’re moved to a separate list. The following code does this without using functions:

# Start with some designs that need to be printed.
unprinted_designs = ['phone case', 'robot pendant', 'cube']
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)

# Display all completed models.
print("\nThe following models have been printed:")
for completed_model in completed_models:
    print(completed_model)
Enter fullscreen mode Exit fullscreen mode
Printing model: cube
Printing model: robot pendant
Printing model: phone case

The following models have been printed:
cube
robot pendant
phone case
Enter fullscreen mode Exit fullscreen mode

We can reorganize this code by writing two functions, each of which does one specific job. Most of the code won’t change; we’re just making it more carefully structured. The first function will handle printing the designs, and the second will summarize the prints that have been made:

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', 'cube']
completed_models = []

print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)
Enter fullscreen mode Exit fullscreen mode
Printing model: cube
Printing model: robot pendant
Printing model: phone case

The following models have been printed:
cube
robot pendant
phone case
Enter fullscreen mode Exit fullscreen mode

This program has the same output as the version without functions, but the code is much more organized. The code that does most of the work has been moved to two separate functions, which makes the main part of the program easier to understand. Look at the body of the program to see how much easier it is to understand what this program is doing.

unprinted_designs = ['phone case', 'robot pendant', 'dodecahedron']
completed_models = []

print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)
Enter fullscreen mode Exit fullscreen mode

We set up a list of unprinted designs and an empty list that will hold the completed models. Then, because we’ve already defined our two functions, all we have to do is call them and pass them the right arguments. We call print_models() and pass it the two lists it needs; as expected, print_models() simulates printing the designs. Then we call show_completed_models() and pass it the list of completed models so it can report the models that have been printed. The descriptive function names allow others to read this code and understand it, even without comments.

This program is easier to extend and maintain than the version without functions. If we need to print more designs later on, we can simply call print_models() again. If we realize the printing code needs to be modified, we can change the code once, and our changes will take place everywhere the function is called. This technique is more efficient than having to update code separately in several places in the program.

This example also demonstrates the idea that every function should have one specific job. The first function prints each design, and the second displays the completed models. This is more beneficial than using one function to do both jobs. If you’re writing a function and notice the function is doing too many different tasks, try to split the code into two functions. Remember that you can always call a function from another function, which can be helpful when splitting a complex task into a series of steps.

Preventing a Function from Modifying a List

Sometimes you’ll want to prevent a function from modifying a list. For example, say that you start with a list of unprinted designs and write a function to move them to a list of completed models, as in the previous example. You may decide that even though you’ve printed all the designs, you want to keep the original list of unprinted designs for your records. But because you moved all the design names out of unprinted_designs, the list is now empty, and the empty list is the only version you have; the original is gone. In this case, you can address this issue by passing the function a copy of the list, not the original. Any changes the function makes to the list will affect only the copy, leaving the original list intact.

You can send a copy of a list to a function like this:

function_name(list_name[:])
Enter fullscreen mode Exit fullscreen mode

The slice notation [:] makes a copy of the list to send to the function. If we didn’t want to empty the list of unprinted designs, we could call print_models() like this:

print_models(unprinted_designs[:], completed_models)
Enter fullscreen mode Exit fullscreen mode

The function print_models() can do its work because it still receives the names of all unprinted designs. But this time it uses a copy of the original unprinted designs list, not the actual unprinted_designs list. The list completed_models will fill up with the names of printed models like it did before, but the original list of unprinted designs will be unaffected by the function.

Even though you can preserve the contents of a list by passing a copy of it to your functions, you should pass the original list to functions unless you have a specific reason to pass a copy. 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 you’re working with large lists.

Passing an Arbitrary Number of Arguments

Sometimes you won’t know ahead of time how many arguments a function needs to accept. Fortunately, Python allows a function to collect an arbitrary number of arguments from the calling statement.

For example, consider a function that builds a pizza. It needs to accept a number of toppings, but you can’t know ahead of time how many toppings a person will want. The function in the following example has one parameter, *toppings, but this parameter collects as many arguments as the calling line provides:

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')
Enter fullscreen mode Exit fullscreen mode
('pepperoni',)
('mushrooms', 'green peppers', 'extra cheese')
Enter fullscreen mode Exit fullscreen mode

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. The print() call in the function body produces output showing that Python can handle a function call with one value and a call with three values. It treats the different calls similarly. Note that Python packs the arguments into a tuple, even if the function receives only one value.

def make_pizza(*toppings):
    """Summarize the pizza we are about to make."""
    print("\nMaking a pizza with the following toppings:")
    for topping in toppings:
        print(f"- {topping}")

make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')
Enter fullscreen mode Exit fullscreen mode

Making a pizza with the following toppings:
- pepperoni

Making a pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
Enter fullscreen mode Exit fullscreen mode

The function responds appropriately, whether it receives one value or three values. This syntax works no matter how many arguments the function receives.

Mixing Positional and Arbitrary Arguments

If you want a function to accept several different kinds of arguments, 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.

For example, if the function needs to take in a size for the pizza, that parameter must come before the parameter *toppings:

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')
Enter fullscreen mode Exit fullscreen mode

Making a 16-inch pizza with the following toppings:
- pepperoni

Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
Enter fullscreen mode Exit fullscreen mode

In the function definition, Python assigns the first value it receives to the parameter size. All other values that come after are stored in the tuple toppings. The function calls include an argument for the size first, followed by as many toppings as needed.

You’ll often see the generic parameter name *args, which collects arbitrary positional arguments like this.

Using Arbitrary Keyword Arguments

Sometimes you’ll want to accept an arbitrary number of arguments, but you won’t know ahead of time what kind of information will be passed to the function. In this case, you can write functions that accept as many key-value pairs as the calling statement provides. One example involves building user profiles: you know you’ll get information about a user, but you’re not sure what kind of information you’ll receive. The function build_profile() in the following example always takes in a first and last name, but it accepts an arbitrary number of keyword arguments as well:

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)
Enter fullscreen mode Exit fullscreen mode

The definition of build_profile() expects a first and last name, and then it allows the user to pass in as many name-value pairs as they want. The double asterisks before the parameter **user_info cause Python to create an empty dictionary called user_info and pack whatever name-value pairs it receives into this dictionary. Within the function, you can access the key-value pairs in user_info just as you would for any dictionary.

In the body of build_profile(), we add the first and last names to the user_info dictionary because we’ll always receive these two pieces of information from the user, and they haven’t been placed into the dictionary yet. Then we return the user_info dictionary to the function call line.

We call build_profile(), passing it the first name 'albert', the last name 'einstein', and the two key-value pairs location='princeton' and field='physics'. We assign the returned profile to user_profile and print user_profile:

{'location': 'princeton', 'field': 'physics', 'first_name': 'albert', 'last_name': 'einstein'}
Enter fullscreen mode Exit fullscreen mode

The returned dictionary contains the user’s first and last names and, in this case, the location and field of study as well. The function would work no matter how many additional key-value pairs are provided in the
function call.

You can mix positional, keyword, and arbitrary values in many different ways when writing your own functions. It’s useful to know that all these argument types exist because you’ll see them often when you start reading other people’s code. It takes practice to learn to use the different types correctly and to know when to use each type. For now, remember to use the simplest approach that gets the job done. As you progress you’ll learn to use the most efficient approach each time.

You’ll often see the parameter name **kwargs used to collect non-specific keyword arguments.

Storing Your Functions in Modules

One advantage of functions is the way they separate blocks of code from your main program. By using descriptive names for your functions, your main program will be much easier to follow. You can go a step further by storing your functions in a separate file called a module and then importing that module into your main program. An import statement tells Python to make the code in a module available in the currently running program file.

Storing your functions in a separate file allows you to hide the details of your program’s code and focus on its higher-level logic. It also allows you to reuse functions in many different programs. When you store your functions in separate files, you can share those files with other programmers without having to share your entire program. Knowing how to import functions also allows you to use libraries of functions that other programmers have written.

There are several ways to import a module.

Importing an Entire Module

To start importing functions, we first need to create a module.

A module is a file ending in .py that contains the code you want to import into your program.

We can make a module contains the function make_pizza() by putting the function definition in a separate file (for example named pizza), the import this file where we need to use.

File pizza. py

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}")
Enter fullscreen mode Exit fullscreen mode

File making_pizzas. py (at the same directory as pizza. py)

import pizza

pizza.make_pizza(16, 'pepperoni')
pizza.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
Enter fullscreen mode Exit fullscreen mode

When Python reads this file, the line import pizza tells Python to open the file pizza. py and copy all the functions from it into this program. You don’t actually see code being copied between files because Python copies the code behind the scenes just before the program runs. All you need to know is that any function defined in pizza.py will now be available in making_pizzas. py.

This first approach to importing, in which you simply write import followed by the name of the module, makes every function from the module available in your program. If you use this kind of import statement to import
an entire module named module_name. py, each function in the module is available through the following syntax:

module_name.function_name()
Enter fullscreen mode Exit fullscreen mode

Importing Specific Functions

You can also import a specific function from a module. Here’s the general syntax for this approach:

from module_name import function_name
Enter fullscreen mode Exit fullscreen mode

You can import as many functions as you want from a module by separating each function’s name with a comma:

from module_name import function_0, function_1, function_2
Enter fullscreen mode Exit fullscreen mode

The making_pizzas. py example would look like this if we want to import just the function we’re going to use:

from pizza import make_pizza

make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
Enter fullscreen mode Exit fullscreen mode

With this syntax, you don’t need to use the dot notation when you call a function. Because we’ve explicitly imported the function make_pizza() in the import statement, we can call it by name when we use the function.

Using as to Give a Function an Alias

If the name of a function you’re importing might conflict with an existing name in your program or if the function name is long, you can use a short, unique alias—an alternate name similar to a nickname for the function. You’ll give the function this special nickname when you import the function.

Here we give the function make_pizza() an alias, mp(), by importing make_pizza as mp. The as keyword renames a function using the alias you provide:

from pizza import make_pizza as mp

mp(16, 'pepperoni')
mp(12, 'mushrooms', 'green peppers', 'extra cheese')
Enter fullscreen mode Exit fullscreen mode

The import statement shown here renames the function make_pizza() to mp() in this program. Any time we want to call make_pizza() we can simply write mp() instead, and Python will run the code in make_pizza() while avoiding any confusion with another make_pizza() function you might have written in this program file.

The general syntax for providing an alias is:

from module_name import function_name as fn
Enter fullscreen mode Exit fullscreen mode

Using as to Give a Module an Alias

You can also provide an alias for a module name. Giving a module a short alias, like p for pizza, allows you to call the module’s functions more quickly. Calling p.make_pizza() is more concise than calling pizza.make_pizza():

import pizza as p

p.make_pizza(16, 'pepperoni')
p.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
Enter fullscreen mode Exit fullscreen mode

The module pizza is given the alias p in the import statement, but all of the module’s functions retain their original names. Calling the functions by writing p.make_pizza() is not only more concise than writing pizza.make_pizza(), but also redirects your attention from the module name and allows you to focus on the descriptive names of its functions. These function names, which clearly tell you what each function does, are more important to the readability of your code than using the full module name.

The general syntax for this approach is:

import module_name as mn
Enter fullscreen mode Exit fullscreen mode

Importing All Functions in a Module

You can tell Python to import every function in a module by using the asterisk (*) operator:

from pizza import *

make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
Enter fullscreen mode Exit fullscreen mode

The asterisk in the import statement tells Python to copy every function from the module pizza into this program file. Because every function is imported, you can call each function by name without using the dot notation. However, it’s best not to use this approach when you’re working with larger modules that you didn’t write: if the module has a function name that matches an existing name in your project, you can get some unexpected results. Python may see several functions or variables with the same name, and instead of importing all the functions separately, it will overwrite the functions.

The best approach is to import the function or functions you want, or import the entire module and use the dot notation. This leads to clear code that’s easy to read and understand.

Styling Functions

Functions should have descriptive names, and these names should use lowercase letters and underscores. Descriptive names help you and others understand what your code is trying to do. Module names should use these conventions as well.

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. They should be able to trust that the code works as described, and as long as they know the name of the function, the arguments it needs, and the kind of value it returns, they should be able to use it in their programs.

If you specify a default value for a parameter, no spaces should be used on either side of the equal sign:

def function_name(parameter_0, parameter_1='default value')
Enter fullscreen mode Exit fullscreen mode

The same convention should be used for keyword arguments in function calls:

function_name(value_0, parameter_1='value')
Enter fullscreen mode Exit fullscreen mode

If a set of parameters causes a function’s definition to be 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 body of the function, which will only be indented one level.

Most editors automatically line up any additional lines of parameters to match the indentation you have established on the first line:

def function_name(
        parameter_0, parameter_1, parameter_2,
        parameter_3, parameter_4, parameter_5):
    function body...
Enter fullscreen mode Exit fullscreen mode

If your program or module has more than one function, you 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 you use comments at the beginning of your file to describe the overall program.

Discussion (1)

Collapse
andrewbaisden profile image
Andrew Baisden

Python is such a nice language and it's really trending hard right now because of PyScript. Good run through on Python functions btw.