A function in Python is a way to perform a specific task. Functions allow you to repeat statements that are used multiple times, increasing the readability of your code by making it more modular.
We can think of functions are a bundling of instructions. They allow us to even pass inputs and generate outputs, meaning that we can use functions to carry out specific tasks for different pieces.
Why Write Functions?
Functions allow you to write modular and reusable code. Let’s break down three main reasons why you’d want to write functions in Python:
Built-in Python Functions
We don’t need to always define our own functions. Some functions are so important that they’re built right into Python.
For example, the print()
function allows us to display output to the console. For example, let’s write the classic 'Hello World!'
message:
# Printing a statement to the console
print('Hello World!')
# Returns:
# Hello World!
In the code block above, we used the print()
function for the first time!
Another common built-in Python function is the len()
function, which returns the length of an items that is passed into it. Let’s check how long the string we used earlier is:
# Checking the length of a string
print(len('Hello world!'))
# Returns:
# 12
Let’s now take a look at how we can define our own functions.
Building Your Own Python Functions
In Python, you can define your own functions using the def
keyword:
def greet():
print('Hi there!')
We use the def
keywork to let Python know we’re defining a function. This is followed by the function name. We use parantheses ()
to hold parameters. In our example above, we’re not using any parameters. Finally, we close the line using a colon :
.
We write the body of our function on the following indented lines.
We can now call the function by write greet()
. Let’s give this a shot:
def greet():
print('Hi there!')
greet()
# Returns:
# Hi there!
By calling our function, we were able to print out our string.
Let’s now work on creating more powerful function by passing in parameters.
Adding Parameters to Functions
We often need to pass information into functions. We can do this by using parameters. Parameters allow us to customize how a function works by passing inputs into it.
You can think of a parameter as a placeholder for a value that we provide when we call a function.
Let’s modify our earlier function greet()
to accept the name of the person we want to greet.
def greet(name):
print("Hi there, " + name)
greet("James")
# Returns:
# Hi there, James
Now that we have set a parameter, the function expects this parameter to be passed in. If we try to call the function without passing anything in, Python will raise a TypeError
:
greet()
# Raises:
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: greet() missing 1 required positional argument: 'name'
We can avoid this by modifying our function to use default arguments. This means that we can call the function without passing in any additional information.
def greet(name='friend'):
print("Hi there, " + name)
greet()
# Returns:
# Hi there, friend
Functions That Return Something
So far, the functions that we have created have only printed out text. Practically speaking, that’s not very useful.
Generally, functions are used to process something and then return that processed value. We can assign that returned value to a variable.
Let’s take a look at a function that adds two numbers:
def add(num1, num2):
return num1 + num2
Now, we can use our function to add two values together. Let’s assign the returned value to a new variable:
summed = add(1, 2)
print(summed)
Running the above code will return: 3
.
Test Your Understanding: Discount Calculator
Write a function called calculate_discounted_price
that takes two arguments:
price
(the original price of an item)discount
(the discount percentage, as a number between 0 and 100)
The function should return the final price after applying the discount. The function handles cases where the discount is invalid (less than 0 or greater than 100).
Functions with Arbitrary Arguments
In some cases, you might not know the number of arguments you need to pass into a function. Imagine, for example, the function add()
you defined earlier needed to accept more than two numbers. You could, of course, define the function to accept three numbers, four numbers, or any kind of amount.
However, you can also simplify this process by using arbitrary arguments. Arbitrary arguments (also known as *args
) allow you to pass a varying number of values into a function call. In order to define this type of argument, you simply need to place an asterisk in front of the parameter name.
def add(*nums):
res = 0
for num in nums:
res += num
return res
In defining the function as shown above, once more than one argument is passed in, the argument nums
behaves like an array. Because of this, you can iterate over it and access each of the values passed into it.
print(add(1,2,3))
# Returns:
# 6
We can see that by using the *args
capability in Python we can simplify our functions significantly.
If you need to pass in a named or positional argument as well as arbitrary ones, the named or positional ones must come before the arbitrary ones. We can see this below:
def add(base, *nums):
for num in nums:
base += num
return res
In the example above, we added a base
argument, which acts as a base number to which the other numbers are added.
Exercise: Greet Everyone!
Write a function that uses arbitrary arguments to print a f”Hi, {name}” for each name entered.
Anonymous, Lambda Functions in Python
Using the lambda
keyword, you can create functions that don’t have a name – or, in other words, are anonymous. By using the lambda
keyword, you can choose to pass in arguments but have to pass in an expression. These functions follow the structure shown below:
lambda argument(s) : expression
One of the perks of lambda functions is their conciseness. For simple one-line functions, a lambda function can be a great alternative. Say we wanted to replace our earlier greet()
function with a lambda function, we could write the following:
greet = lambda: print("Hi there!")
greet()
# Returns:
# Hi there!
We can extend this further by adding parameters into the function. This will allow us to pass in, for example, names into the function:
greet = lambda name: print(f"Hi there, {name}!")
greet("James")
# Returns:
# Hi there, James!
In many cases, you’ll see lambda functions applied as one-offs in other functions. For example, you may want to define a key
when using the sorted()
function to sort some iterable. Rather than creating a name-space function (i.e., using def
), we can create a lambda function.
Say, for example, we had a list of tuples that contained a name and an age, such as the one shown below:
people = [("Alice", 25), ("Bob", 30), ("Charlie", 22)]
We can use the built-in sorted()
function to sort this list of tuples. However, by default this function will sort the list based on the tuples’ first items.
sorted_list = list(sorted(people))
print(sorted_list)
# Returns:
# [("Alice", 25), ("Bob", 30), ("Charlie", 22)]
If we wanted to sort the list by the person’s age, we would need to find a way to access the tuples’ second items. A lambda function is perfect for this, especially if we only ever need this function the one time.
sorted_people = list(sorted(people, key=lambda x: x[1]))
print(sorted_people)
# Returns:
# [('Charlie', 22), ('Alice', 25), ('Bob', 30)]
Annotating Functions with Docstrings and Type Hints
Clear and well-documented code makes your code easier to read and understand. Python provides two great tools to accomplish this:
- Docstrings, which are strings placed right after a function is defined to describe what the function does, and
- Type hints, which allow you to describe what the expected types of function parameters and return values are.
Let’s take a look at our previous add()
function, which summed two numbers:
def add(num1, num2):
return num1 + num2
Logically, it should make sense what the function does – it adds numbers. But what kind of numbers (integers, floats, etc.?) and how many numbers? In this case the function expects exactly two numbers, but that may not be clear unless you read the way in which the function is defined.
In this case, using a docstring would make this much clearer.
By convention, we define a docstring using triple quotes ("""
) on the line immediately following the def
keyword.
def add(num1, num2):
"""Adds two numbers together."""
return num1 + num2
On this page, this might not look like much. However, this now adds a helpful hint when you hover over the function in a code editor. Similarly, we can now use the Python help()
function to get more information about the function.
help(add)
# Returns:
# Help on function add in module __main__:
# add(num1, num2)
# Adds two numbers together.
By convention, you’d add more information than this one-liner. Python encourages you to add additional information, such as about the arguments and return values on a line following a linebreak. Let’s add a little bit more information now:
def add(num1, num2):
"""Adds two numbers together.
Args:
num1: The first number
num2: The second number
Returns:
The sum of num1 and num2
"""
return num1 + num2
It’s now immediately clear what the function does. It’s still not completely clear, however, what the function expects. In order to accomplish this, we can use type hints.
As the name implies, type hints provide hints to the user around what data type a variable expects. Because Python is dynamically-typed, this won’t actually enforce any type checking, but it provides helpful guidance.
In Python, we can add a type hint using the :
character following a variable. To type hint a return value, we use the ->
characters prior to closing the definition statement. Let’s see how we can annotate our function further.
def add(num1: int | float, num2: int | float) -> int | float:
"""Add two numbers together.
Args:
num1 (int | float): The first number.
num2 (int | float): The second number.
Returns:
int | float: The sum of the two numbers.
"""
return num1 + num2
While this makes your code a little more verbose, it also gives your code’s users a much better understanding of how to use the function.
Type Hinting Multiple Types
Note that we’re using the pipe operator |
in the code block above. This functionality is only available in Python 3.10+.
Prior to this, you would have had to import the Union
class from the typing
module using from typing import Union
. This would make your code look like this:
def add(num1: Union[int, float], num2:
Union[int, float]
) -> Union[int, float]
:
In summary, docstrings provide instructions of what your function does, while type hints provide insight into what type of data the function is looking for.