DEV Community

Mark Edosa
Mark Edosa

Posted on • Updated on

Introduction to Python Programming - Function Basics

Table of Content

Introduction

At the end of this article, you will have learned the following:

  • how to define and use your functions
  • how to use function parameters and return statements
  • built-in functions
  • lambda functions for one-time-only tasks
  • how to create a function to meet the requirements of a "real-life" scenario.

I assume that you've read the previous articles in this series. Also, you are either a beginner programmer or an experienced programmer learning. Let's get started :)

Let's say you want to calculate the body mass index (BMI) of 20 people. Maybe under different conditions or in other parts of your program. Typically you might be tempted to go straight to the calculation.

Let's assume the height and weight of the first person were 1.71m and 68kg, respectively.


# calculate BMI

# height in meters
height = 1.71

# weight in kilograms
weight = 68

# bmi = weight/height square
bmi = weight / height**2

print(bmi)
Enter fullscreen mode Exit fullscreen mode

How about the second person? You copy the code to where you need it.


# calculate BMI

# height in meters
height = 1.89

# weight in kilograms
weight = 92

# bmi = weight/height square
bmi = weight / height**2

print(bmi)

Enter fullscreen mode Exit fullscreen mode

The third person? Or in a different file? You copy again.

That is way too much repetition that you want to avoid. Luckily, Python functions allow you to group or organize your codes.

An inherent benefit of using functions, apart from logic grouping, is the expression of your intention. Function names are a great way to express your intent to yourself and others who will read your code. For example, let's rewrite the above code that calculates BMI.


# define the function
def calculate_bmi(height, weight):
    bmi = weight / height**2
    return bmi

# call the function and save its output in 
# the result variable
result = calculate_bmi(1.71, 68)

print(result)

Enter fullscreen mode Exit fullscreen mode

You don't have to look at the codes line by line to know what it is doing. It calculates BMI.

In Python, two categories of functions exist - Built-In functions provided by the standard library and user-defined functions (the ones you define yourself). Let's start with user-defined functions since they will enable us to understand Built-In functions.

Defining Functions

To define a function, start by using the def keyword. Followed by the name of the function, a parenthesis, a colon (:), and then the function body. The format is:

def function_name():
    # Do something here in the function body


def second_function(parameter_1, parameter_2, ..., parameter_N):
    # do something here

    return something # to get the result
Enter fullscreen mode Exit fullscreen mode

The function name must follow the rules on variable naming. The parenthesis can contain 0 or more implicitly-defined variables called parameters or arguments. The function body can contain a return statement depending on whether the function generates an output.

Let's examine the calculate_bmi function we created above. It has a name (stating the obvious) and has two parameters (height and weight). It computes the BMI within its body (bmi = weight / height**2) and uses the return keyword to give back the result of the calculation (return bmi). Here's the function again for reference.

def calculate_bmi(height, weight):
    bmi = weight / height**2
    return bmi

Enter fullscreen mode Exit fullscreen mode

Calling a Function

To use (call) a function, write its name and include a parenthesis with or without arguments (or parameters). For example, call the calculate_bmi function like so:

calculate_bmi(1.91, 80)
Enter fullscreen mode Exit fullscreen mode

If the function returns a value, we can (optionally) save the value in a variable.

my_bmi = calculate_bmi(1.91, 80)
print(my_pmi)
Enter fullscreen mode Exit fullscreen mode

Note: Any function that does not explicitly return a value will return None by default.

Function Parameters and Arguments

As earlier stated, parameters/arguments are variables implicitly created based on function definition. They act as the function's inputs.

A function can take multiple parameters as in the calculate_bmi function. Also, a function can have no parameter. For example:

# define a function that takes no argument/parameter
def print_hello_world():
    print("Hello World!")

Enter fullscreen mode Exit fullscreen mode
# call the function
print_hello_world()
Enter fullscreen mode Exit fullscreen mode

One or more arguments can have default values. A function uses the default values if you call it without specifying the argument. For example, sum_num is a function that adds two numbers. By default, those numbers are 5 and 10.

def sum_num(num1 = 5, num2 = 10):
  result = num1 + num2
  return result
Enter fullscreen mode Exit fullscreen mode

We can use sum_num in several ways.

# although no arguments are passed in
# num1 is 5 and num2 is 10. The defaults.
fifteen = sum_num()

# returns 25 since num1 is 13 and num2 is 12
twenty_five = sum_num(13, 12)

# specify num1 (20) and use num2's default value (10)
thirty = sum_num(20)
Enter fullscreen mode Exit fullscreen mode

But wait! How do I specify num2 alone? This question brings us to the concept of positional and named arguments.

Positional and Named (Keyword) Arguments

Let's start by defining a different version of sum_num say sum_num_v2.


# function with 1 positional required argument. 
# num1 and num2 are optional named/keyword arguments
def sum_num_v2(num1, num2 = 5, num3 = 10):
  return num1 + num2 + num3
Enter fullscreen mode Exit fullscreen mode

You can use the return keyword in the same line as the calculation.

We can pass arguments to a function based on the position of the arguments or based on the names (keywords) of the arguments. The function sum_num_v2 has one positional argument num1 and two named arguments num2 and num2. The required position argument num1 must always be in the first position during the function call while the named arguments can either take position 2 or 3. For example

# num1, num2, num3 take their positions - 1,2 and 3 respectively
sum_num_v2(10, 20, 30)

# This is the same as using the positions above
sum_num_v2(10, num2 = 20, num3 = 30)

# The order of num2 and num3 have been switched
# We get the same result as above since
sum_num_v2(10, num3 = 30, num2 = 20)

# we could omit num2 and num3 since they
# have default values.
sum_num_v2(10)
Enter fullscreen mode Exit fullscreen mode

Calling a function without its required arguments throws an error.

# sum_num_v2() # fails :(
Enter fullscreen mode Exit fullscreen mode

In recent Python versions, you can be strict about the number and placement of positional and keyword arguments. For example, consider the describe_me function below:

def describe_me(name, age, city=None, zipcode=None):
  # do nothing
  pass

Enter fullscreen mode Exit fullscreen mode

You can call the function in two ways:

  • with four positional arguments describe_me("Johnny Depp", 1020, "Caribbean", "123456")
  • with two positional and two keyword arguments describe_me("Johnny Depp", 1020, city="Caribbean", zipcode="123456")

What if you wanted to be very specific? You can use \ (positional) and/or * (keyword) to restrict the function's use to a single way like so:


def describe_me(name, age, /, *, city=None, zipcode=None):
  pass

# Fails
# describe_me("Peter Griffin", 48, "Quahog", "00093")
# TypeError: describe_me() takes 2 positional arguments but 4 were given

# works!
describe_me("Peter Griffin", 48, city="Quahog", zipcode="00093")
Enter fullscreen mode Exit fullscreen mode

The first two arguments must be positional arguments while the last two must be keyword arguments. You can't call the function with four positional arguments.

Likewise, you can also restrict a function to use only keyword arguments like so:

def I_dont_take_positions(*, city=None, zipcode=None):
  pass


# Fails
# I_dont_take_positions() takes 0 positional arguments but 2 were given
# I_dont_take_positions("Quahog", "00093")

# works!
I_dont_take_positions(city="Quahog", zipcode="00093")

# works! the order does not matter remember?
I_dont_take_positions(zipcode="00093", city="Quahog")
Enter fullscreen mode Exit fullscreen mode

Return Statement

Earlier, you saw the return keyword in the calculate_bmi, sum_num, and sum_num_v2. These functions did some calculations and used the return statement to give back the results.

# Store the calculation in the result variable
result = sum_num_v2(10, 20, 30)

print(result)
Enter fullscreen mode Exit fullscreen mode

In other cases, you can return nothing (None) from a function if you use the return keyword alone. For example, the calculate_bmi function requires the weight and height parameters. What if they're absent? What if you give strings instead?

One way to defend your function is to return early if the parameters are not valid:


# helper function: check if parameter n is a number 
def is_number(n):
  if isinstance(n, int) or isinstance(n, float):
    return True

  return False

# A safer, better version of calculate_bmi

def calculate_bmi_v2(weight, height):
  # Do not calculate BMI if weight or height is not a number
  # return early
  if not is_number(weight) or not is_number(height):
    return 

  bmi = weight / height**2
  return bmi
Enter fullscreen mode Exit fullscreen mode
# works well
print(calculate_bmi_v2(68, 1.61))

# returns None
print(calculate_bmi_v2("Hello", 1))

print(calculate_bmi_v2(1, "Hello"))

# returns None
print(calculate_bmi_v2("Hello", "World"))

Enter fullscreen mode Exit fullscreen mode

Note that in the absence of a return statement, a function returns None implicitly.

You can return a single value or multiple values from a function. To do so, separate the return values with a comma. For example, the generate_123 function below returns three different values-1,2,3.


def generate_123():
  return 1, 2, 3

Enter fullscreen mode Exit fullscreen mode

The result of the function call is a tuple containing the values.

one_two_three = generate_123()

print(one_two_three)
Enter fullscreen mode Exit fullscreen mode

You could also unpack the return values individually.

# Use tuple unpacking
one, two, three = generate_123()

print(one, two, three)

Enter fullscreen mode Exit fullscreen mode

You can use the return statement within an if-statement, a for loop, or a while loop. The function will exit. For example:


def contains_only_even_numbers(numbers):
  for num in numbers:
    # If it's an odd number return False
    if num % 2 != 0:
      return False

  # If we get here, the numbers parameter contains only even numbers
  return True

Enter fullscreen mode Exit fullscreen mode
outcome = contains_only_even_numbers([2, 4, 6])
print(outcome)  # True

outcome = contains_only_even_numbers([1, 2, 4]) # False
print(outcome)  # False

Enter fullscreen mode Exit fullscreen mode

Built-In Functions

Python provides many functions in its standard library to make development easier.

These functions allow you to perform common operations without writing them yourself. For example, you can replace the functions sum_num and sum_num_v2 with the sum function provided by Python.

numbers = [1, 2, 3, 4, 5]

total = sum(numbers)

print(total) # 15
Enter fullscreen mode Exit fullscreen mode

The table below shows some common Built-In functions:

Function Name Description Example
int Convert a string/float to an integer int(4.8)
float Convert a string/integer to a float float("11.82")
str Convert any type to a string str(1)
type Check the type of a variable/literal type(5)
map Apply a function to every element of a sequence/collection map(your_function, collection)
filter Apply a predicate to a collection filter(your_function, collection)
any Check if any item in a collection is True. Use with map any(map(predicate, collection))
all Check if all item in a collection are True. Use with map all(map(predicate, collection))
enumerate Add indices to a sequence/collection enumerate(numbers)
zip Combine collections zip(numbers, letters)
min Get the minimum value in a sequence/collection min([1,2,3])
max Get the maximum value in a sequence/collection max("xab")
sorted Sort a sequence/collection in a customized way sorted([2, 1, 5], reverse=True)
reversed Reverse the content of a sequence list(reversed([2, 4, 5, 1]))
open Open a file for reading/writing/appending. open("my_file.txt", mode="r", encoding="utf8")
help Provide helpful message on variables, functions, etc. help(print)

A predicate function returns a boolean-True or False

Let's use some of the functions. Start by creating some collections names and numbers

# some names
names = ["John Doe", "Peter Griffin", "Ghost Busters", "Super Mario"]

# numbers 1 - 100
numbers = list(range(1, 101))

Enter fullscreen mode Exit fullscreen mode

map: apply a function to every element of a sequence/collection/iterable (list, tuple, etc.)

Create two functions square and length. square raises a number to power 2. length gives the length of a string.

def square(num):
  return num**2

def length(name):
  return len(name)

Enter fullscreen mode Exit fullscreen mode

Square all the numbers within the numbers list.

squared_numbers = map(square, numbers)

# squared_number is wrapped (a map object)
print(squared_numbers)

# Convert to a list/tuple to view the actual content
squared_numbers = list(squared_numbers)
print(squared_numbers)

Enter fullscreen mode Exit fullscreen mode

Get the length of each name in the names list.

# length names are wrapped (a map object)
length_names = map(length, names)

# Convert to a tuple and view the content
print(tuple(length_names))

# CAREFUL HERE: # tuple is empty because we already used the content
# To avoid this scenario, save the content of the map object.
print(tuple(length_names)) 
Enter fullscreen mode Exit fullscreen mode

The map function returns a map object, a collection that's not evaluated instantly. The operation runs when you use list or tuple on the map object. Note You shouldn't use list or tuple on the same map object twice. Else, the content of the second call will be empty.

Instead of storing and converting a map object, you can loop through its content. For example:

for length_of_name in map(length, names):
  print(length_of_name)
Enter fullscreen mode Exit fullscreen mode

filter: apply a predicate function to each element of an iterable/sequence

Let's say you only want some names. For example, names that have 13 letters or less. Or names that start with the letter P. Use the filter function here.

def less_or_eq_13(name):
  return len(name) <= 13

def starts_with_p(name):
  return name.lower().startswith("p")
Enter fullscreen mode Exit fullscreen mode
names_lte_13 = filter(less_or_eq_13, names)

pnames = filter(starts_with_p, names)

for name in names_lte_13:
  print(name)

# Just like `map`, `filter` returns a filter object that is used only once
# For multiple uses, save the content in a variable
print(list(names_lte_13))
Enter fullscreen mode Exit fullscreen mode

any and all

def is_multiple_of_5(n): return n % 5 == 0

def is_odd(n): return n % 2 != 0
Enter fullscreen mode Exit fullscreen mode

# check if any of the elements is a multiple of 5
mult_5 = any(map(is_multiple_of_5, numbers))
print(mult_5)

# Check if numbers contain only odd numbers
all_odds = all(map(is_odd, numbers))
print(all_odds)
Enter fullscreen mode Exit fullscreen mode

enumerate

enumerate associates an index with each element of collection/sequence. enumerate returns an enumerate object that can be converted to a list of tuples.

indices_and_names = enumerate(names)

# enumerate object
print(indices_and_names)

# Convert to a list and print
print(list(indices_and_names))
Enter fullscreen mode Exit fullscreen mode

Like map objects and filter objects, you can loop through an enumerate object. For example:


for index, name in enumerate(names):
  print(f"{name} is at position {index}")

Enter fullscreen mode Exit fullscreen mode

zip

Using zip, you can combine sequences to create a zip object. Consequently, the zip object can be converted into a list of tuples or used in a for loop.

For example, you can combine the names list with the length_names to have both a name and its length side by side.

# Create a zipped object
names_length_zipped = zip(names, length_names)

# loop through
for name, length in names_length_zipped:
  print(f"{name} has {length} letters")
Enter fullscreen mode Exit fullscreen mode

You can combine the zip step and the for loop.

# Do the above in a single step
for name, length in zip(names, length_names):
  print(f"{name} has {length} letters")

Enter fullscreen mode Exit fullscreen mode

Detour: Implementing the Map and Filter Functions Yourself

The map and filter functions are similar to the map_ and filter_ defined below.

# many people prefer def map_(f, items)
# I chose to write mine in full for demo purposes
def map_(function, items):
  result = []
  for item in items:
    # Call the function and append its output to result
    result.append(function(item))

  return result


# many people prefer def filter_(f, items)
# I chose to write mine in full for demo purposes
def filter_(function, items):
  result = []
  for item in items:
    if function(item):
      result.append(item)

  return result

Enter fullscreen mode Exit fullscreen mode

Note that these functions map_ and filter_ are evaluated immediately. Don't use in real code.

Lambda Functions

If the question "Do I have to define a function each time I use map and filter?" comes to your mind, the answer is NO!

With lambdas you can define a small function where you need it and forget it never existed. Lambdas also provide the following advantages:

  • Lambda functions are nameless. No need to put thought into naming such functions.
  • You won't have to litter your Python script with functions you'll only use once. For example, square, length, is_multiple_of_5, is_odd, less_or_eq_13, and starts_with_p ought to be lambdas.

Excited? How do I create a lambda function?

A lambda function has the following structure: lambda *0_or_more_parameters* : *function body*. No parenthesis or explicit return statement is required. Implicitly, Python returns the result of the expression in the function body.

Let's rewrite some of the lines above using lambdas. First, ditch the square and length functions.

# Initially, you used a square function.
# Replace with a lambda that does exactly the same thing
squared_numbers = map(lambda x: x**2, numbers)

# Convert to a list/tuple to view the actual content
squared_numbers = list(squared_numbers)
print(squared_numbers)

Enter fullscreen mode Exit fullscreen mode
# No need for the length function you created above
length_names = map(lambda name: len(name), names)

# Convert to a tuple and view the content
print(tuple(length_names))
Enter fullscreen mode Exit fullscreen mode

The filter function is next. Let's select only names where the length of the name is less than or equal to 13 using a lambda function.

names_lte_13 = filter(lambda name: len(name) <= 13, names)

pnames = filter(lambda name: name.lower().startswith('p'), names)

print(list(names_lte_13))
print(list(pnames))
Enter fullscreen mode Exit fullscreen mode

How about the any and all?

# check if any of the elements is a multiple of 5
mult_5 = any(map(lambda n: n % 5 == 0, numbers))
print(mult_5)

# Check if numbers contain only odd numbers
all_odds = all(map(lambda n: n % 2 != 0, numbers))
print(all_odds)
Enter fullscreen mode Exit fullscreen mode

An Example: Validate a Full Name

Imagine that your boss says that the name of a user of your application must meet the following requirements:

  • A space should separate first name and last name
  • First name and last name must be an alphabet
  • First name and last name must have at least three letters
  • The function should return True or False
  • The function must have two versions-one that uses a for loop and another that uses map

To get you started, your colleague creates two functions that must print OK! if your code is correct.


def assert_true(result):
  if result is True:
    print("OK!")
    return

  raise ValueError("incorrect!")


def assert_false(result):
  if result is False:
    print("OK!")
    return

  raise ValueError("incorrect!")

Enter fullscreen mode Exit fullscreen mode

Next, your colleague adds the following calls/tests to prove your function works.


# Test for valid names using both versions 1 and 2

assert_true(valid_name("Mark Edosa"))
assert_true(valid_name("Jet Lis"))

assert_true(valid_name_v2("Mark Edosa"))
assert_true(valid_name_v2("Jet Lis"))


# Test for invalid names using both versions

assert_false(valid_name("a b"))
assert_false(valid_name("Mark"))
assert_false(valid_name("abc"))

assert_false(valid_name_v2("a b"))
assert_false(valid_name_v2("Mark"))
assert_false(valid_name_v2("abc"))

Enter fullscreen mode Exit fullscreen mode

solution
def valid_name(name):

  name_list = name.split()

  if len(name_list) != 2:
    return False

  for name in name_list:
    test = name.isalpha() and len(name) >= 3
    if not test:
      return False

  return True 


def valid_name_v2(name):
  name_list = name.split()

  if len(list_names) != 2:
    return False

  tests = map(lambda name: name.isalpha() and len(name) >= 3, list_names)
  return all(tests)

Enter fullscreen mode Exit fullscreen mode

Conclusion

Functions help you organize your code. They also help you express your intentions through appropriate names.

In this article, you learned how to define and use your functions. You learned about function parameters and return statements. You also saw some Built-In functions in action. And you saw how to use lambda functions for one-time-only tasks. Finally, you (hopefully) created a function to meet the requirements of a "real-life" scenario.

Thank you for reading.

Top comments (0)