3  Functions

You have probably noticed, but if not, we have been using built-in functions in the previous chapters already. Python ships with a number of useful functions that are readily at your disposal. If you want or need to build your own, that is also possible.

3.1 Built-in functions

Let’s take a look at a few built-in functions and how they can be used. Here are a few examples (some of which we’ve already encountered):

  • print(): prints the given argument(s) to the console
  • type(): returns the type of the given argument
  • len(): returns the length of the given argument

Using basic functions is dead simple. Usually the function has some arguments which the user passes on to the function as input. Let’s try the functions above to get a feel for how they work:

my_list = ["Hello", "I'm", 1, "list", True]

print(my_list)
['Hello', "I'm", 1, 'list', True]

So, we see that the print() function can be used to print out the contents of the input parameter into the console. Makes sense, right?

If we inspect the type of the my_list variable, we can (rather unsurprisingly) see that it is a list:

type(my_list)
list

We can also confirm that the third element of the list is an integer:

type(my_list[2])
int

And finally, our list contains 5 elements in total, as we can see when we pass my_list to the len() function:

len(my_list)
5

That’s the basics of using built-in functions. There are loads of other functions, but we won’t cover them here. They are best learned when needed. Next we’ll see how to build our own functions.

3.2 User-defined functions

Custom-made functions are a way to encapsulate code that you want to reuse. They are defined using the def keyword, followed by the function name, and a colon.

# creating a custom function
def my_function():
    # enter the code you wish to run below
    # note that the indentation is important here as well
    print("Hello I'm a custom-made function!")

# calling the function
my_function()
Hello I'm a custom-made function!

Although the function above isn’t particulrly useful, it is still a valid function. We can make our functions more useful by adding some parameters when defining them.

# creating a custom function with parameters
def my_function_with_args(name):
    print(f"Hello {name}, I'm a custom-made function with arguments!")

# we can now pass an argument to the function when calling it
my_function_with_args("John")
Hello John, I'm a custom-made function with arguments!

We can also return values from functions. This is done using the return keyword.

def num_squared(num = 2):
    returned_value = num ** 2
    return returned_value

num_squared()
4

As we saw above, we can also set default values for the parameters of the function. The default value will be used if the user doesn’t pass any arguments to the function.

Note

Variables defined inside a function are not accessible outside of it. For example, we can’t directly call the returned_value variable outside of the num_squared function. This is called the scope of the variable.

3.2.1 Documenting your function

It is a good practice to document your functions. This is done by adding something called a docstring to the function. A docstring is a string that is placed at the beginning of the function and is enclosed in triple quotes. You can include text that describes what the function does and how it is used, i.e., what arguments it takes, and what it returns. This is especially useful with more complex functions.

def my_function_with_args(name):
    """
    This function takes a name as an argument and prints a greeting.
    """
    print(f"Hello {name}, I'm a custom-made function with arguments!")

The docstring can be accessed by using the __doc__ attribute of the function.

print(my_function_with_args.__doc__)

    This function takes a name as an argument and prints a greeting.
    

3.3 Lambda expressions

Lamdas are a way to create small, anonymous functions. They are defined using the lambda keyword, followed by the arguments, a colon, and the expression to evaluate.

Let’s say we have a simple function, which can be described in one line of code. We can naturally define a proper function like this:

def add(a, b):
    return a + b

However, we can also accomplish the same by using a lambda expression:

lambda a, b: a + b
<function __main__.<lambda>(a, b)>

We can use the following syntax to pass arguments to the lambda expression for evaluation:

(lambda a, b: a + b)(2, 3)
5

Now, why would we want to do this you might ask?

Let’s say we want to raise a bunch of numbers to the second power. We could do this by first defining a function and the passing it on to the map() function, which applies the function to each element of a list.

def squared(x):
    return x ** 2

list(map(squared, [1, 2, 3, 4, 5]))
[1, 4, 9, 16, 25]

This works, and is perfectly fine. However, the squared() function is quite simple, so it seems a bit silly to write a separate function for this. Here is where the lambda expression comes in handy. It allows us to accomplish the same thing in a more concise way:

# same with a lambda expression
list(map(lambda x: x ** 2, [1, 2, 3, 4, 5]))
[1, 4, 9, 16, 25]

We get the same result, but with less code. Lambda expressions are especially useful when you need to pass a simple function as an argument to another function. We will work more with them once we familiarize ourselves with Pandas DataFrames.

3.4 Methods

What are methods and how do they differ from functions? In Python, methods are functions that are associated with an object. They are called using the dot notation, i.e., object.method(). A common methods associated with strings is the upper() method, which converts all characters in a string to uppercase. Let’s take a look shall we?

my_string = "hello, I'm a string"

my_string.upper()
"HELLO, I'M A STRING"

You can browse methods associated with an object by using the dir() function. This will return a list of all the methods associated with the object.

# storing the methods associated with the my_string object in a variable
methods_for_my_string = dir(my_string)

# printing the last 5 methods
methods_for_my_string[-5:]
['swapcase', 'title', 'translate', 'upper', 'zfill']

You can also use the help() function to get more information about a method. This will return a description of the method, as well as the arguments it takes.

help(my_string.upper)
Help on built-in function upper:

upper() method of builtins.str instance
    Return a copy of the string converted to uppercase.

What makes methods so powerful is that they can be chained together. This means that you can call multiple methods on the same object in a single line of code. Let’s see an example of this:

my_string.upper().split()
['HELLO,', "I'M", 'A', 'STRING']

3.5 Using help() for functions and classes

Help is a great way to get more information about a method, especially when you are not sure how to use it. It also works for functions and classes.

help(help)
Help on _Helper in module _sitebuiltins object:

class _Helper(builtins.object)
 |  Define the builtin 'help'.
 |  
 |  This is a wrapper around pydoc.help that provides a helpful message
 |  when 'help' is typed at the Python interactive prompt.
 |  
 |  Calling help() at the Python prompt starts an interactive help session.
 |  Calling help(thing) prints help for the python object 'thing'.
 |  
 |  Methods defined here:
 |  
 |  __call__(self, *args, **kwds)
 |      Call self as a function.
 |  
 |  __repr__(self)
 |      Return repr(self).
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables
 |  
 |  __weakref__
 |      list of weak references to the object
help(list)
Help on class list in module builtins:

class list(object)
 |  list(iterable=(), /)
 |  
 |  Built-in mutable sequence.
 |  
 |  If no argument is given, the constructor creates a new empty list.
 |  The argument must be an iterable if specified.
 |  
 |  Methods defined here:
 |  
 |  __add__(self, value, /)
 |      Return self+value.
 |  
 |  __contains__(self, key, /)
 |      Return key in self.
 |  
 |  __delitem__(self, key, /)
 |      Delete self[key].
 |  
 |  __eq__(self, value, /)
 |      Return self==value.
 |  
 |  __ge__(self, value, /)
 |      Return self>=value.
 |  
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |  
 |  __getitem__(...)
 |      x.__getitem__(y) <==> x[y]
 |  
 |  __gt__(self, value, /)
 |      Return self>value.
 |  
 |  __iadd__(self, value, /)
 |      Implement self+=value.
 |  
 |  __imul__(self, value, /)
 |      Implement self*=value.
 |  
 |  __init__(self, /, *args, **kwargs)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  __iter__(self, /)
 |      Implement iter(self).
 |  
 |  __le__(self, value, /)
 |      Return self<=value.
 |  
 |  __len__(self, /)
 |      Return len(self).
 |  
 |  __lt__(self, value, /)
 |      Return self<value.
 |  
 |  __mul__(self, value, /)
 |      Return self*value.
 |  
 |  __ne__(self, value, /)
 |      Return self!=value.
 |  
 |  __repr__(self, /)
 |      Return repr(self).
 |  
 |  __reversed__(self, /)
 |      Return a reverse iterator over the list.
 |  
 |  __rmul__(self, value, /)
 |      Return value*self.
 |  
 |  __setitem__(self, key, value, /)
 |      Set self[key] to value.
 |  
 |  __sizeof__(self, /)
 |      Return the size of the list in memory, in bytes.
 |  
 |  append(self, object, /)
 |      Append object to the end of the list.
 |  
 |  clear(self, /)
 |      Remove all items from list.
 |  
 |  copy(self, /)
 |      Return a shallow copy of the list.
 |  
 |  count(self, value, /)
 |      Return number of occurrences of value.
 |  
 |  extend(self, iterable, /)
 |      Extend list by appending elements from the iterable.
 |  
 |  index(self, value, start=0, stop=9223372036854775807, /)
 |      Return first index of value.
 |      
 |      Raises ValueError if the value is not present.
 |  
 |  insert(self, index, object, /)
 |      Insert object before index.
 |  
 |  pop(self, index=-1, /)
 |      Remove and return item at index (default last).
 |      
 |      Raises IndexError if list is empty or index is out of range.
 |  
 |  remove(self, value, /)
 |      Remove first occurrence of value.
 |      
 |      Raises ValueError if the value is not present.
 |  
 |  reverse(self, /)
 |      Reverse *IN PLACE*.
 |  
 |  sort(self, /, *, key=None, reverse=False)
 |      Sort the list in ascending order and return None.
 |      
 |      The sort is in-place (i.e. the list itself is modified) and stable (i.e. the
 |      order of two equal elements is maintained).
 |      
 |      If a key function is given, apply it once to each list item and sort them,
 |      ascending or descending, according to their function values.
 |      
 |      The reverse flag can be set to sort in descending order.
 |  
 |  ----------------------------------------------------------------------
 |  Class methods defined here:
 |  
 |  __class_getitem__(...) from builtins.type
 |      See PEP 585
 |  
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |  
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  __hash__ = None

That’s it for functions and methods. We will use them extensively in the upcoming chapters, so make sure you understand how they work.

3.6 Conclusion

The first three chapters introduced us to the basics of Python programming. Next we will start discussing how to use the language for processing and analyzing data. In case you are looking to strengthen your understanding of the fundamentals, the Python tutorial on the official Python website is a great place to learn more (Python Software Foundation (2024)).