Python 101 – An Intro to Functions

Functions are reusable pieces of code. Anytime you find yourself writing the same code twice, that code should probably go in a function.

For example, Python has many built-in functions, such as dir() and sum(). You also imported the math module and used its square root function, sqrt().

In this tutorial you will learn about:

  • Creating a function
  • Calling a function
  • Passing arguments
  • Type hinting your arguments
  • Passing keyword arguments
  • Required and default arguments
  • *args and **kwargs
  • Positional-only arguments
  • Scope

Let’s get started!

Creating a Function

A function starts with the keyword, def followed by the name of the function, two parentheses, and then a colon. Next, you indent one or more lines of code under the function to form the function “block”.

Here is an empty function:

def my_function():
    pass

When you create a function, it is usually recommended that the name of the function is all lowercase with words separated by underscores. This is called snake-case.

The pass is a keyword in Python that Python knows to ignore. You can also define an empty function like this:

def my_function():
    ...

In this example, the function has no contents besides an ellipses.

Let’s learn how to use a function next!

Calling a Function

Now that you have a function, you need to make it do something. Let’s do that first:

def my_function():
    print('Hello from my_function')

Now instead of an ellipses or the keyword pass, you have a function that prints out a message.

To call a function, you need to write out its name followed by parentheses:

>>> def my_function():
...     print('Hello from my_function')
... 
>>> my_function()
Hello from my_function

That was nice and easy!

Now let’s learn about passing arguments to your functions.

Passing Arguments

Most functions let you pass arguments to them. The reason for this is that you will normally want to pass one or more positional arguments to a function so that the function can do something with them.

Let’s create a function that takes an argument called name and then prints out a welcome message:

>>> def welcome(name):
...     print(f'Welcome {name}')
... 
>>> welcome('Mike')
Welcome Mike

If you have used other programming languages, you might know that some of them require functions to return something. Python automatically returns None if you do not specify a return value.

Let’s try calling the function and assigning its result to a variable called return_value:

>>> def welcome(name):
...     print(f'Welcome {name}')
... 
>>> return_value = welcome('Mike')
Welcome Mike
>>> print(return_value)
None

When you print out the return_value, you can see that it is None.

Type Hinting Your Arguments

Some programming languages use static types so that when you compile your code, the compiler will warn you of type-related errors. Python is a dynamically typed language, so that doesn’t happen until run time

However, in Python 3.5, the typing module was added to Python to allow developers to add type hinting to their code. This allows you to specify the types of arguments and return values in your code, but does not enforce it. You can use external utilities, such as mypy (http://mypy-lang.org/) to check that your code base is following the type hints that you have set.

Type hinting is not required in Python and it is not enforced by the language, but it is useful when working with teams of developers, especially when the teams are made up of people who are unfamiliar with Python.

Let’s rewrite that last example so that it uses type hinting:

>>> def welcome(name: str) -> None:
...     print(f'Welcome {name}')
... 
>>> return_value = welcome('Mike')
Welcome Mike
>>> print(return_value)
None

This time when you put in the name argument, you end it with a colon (:) followed by the type that you expect. In this case, you expect a string type to be passed in. After that you will note the -> None: bit of code. The -> is special syntax to indicate what the return value is expected to be. For this code, the return value is None.

If you want to return a value explicitly, then you can use the return keyword followed by what you wish to return.

When you run the code, it executes in exactly the same manner as before.

To demonstrate that type hinting is not enforced, you can tell Python to return an integer by using the return keyword:

>>> def welcome(name: str) -> None:
...     print(f'Welcome {name}')
...     return 5
... 
>>> return_value = welcome('Mike')
Welcome Mike
>>> print(return_value)
5

When you run this code, you can see the type hint says that the return value should be None, but you coded it such that it returns the integer 5. Python does not throw an exception.

You can use the mypy tool against this code to verify that it is following the type hinting. If you do so, you will find that it does show an issue. You will learn how to use mypy in Part II of this book.

The main takeaway here is that Python supports type hinting. Python does not enforce types though. However, some Python editors can use type hinting internally to warn you about issues related to types, or you can use mypy manually to find issues.

Now let’s learn what else you can pass to a function.

Passing Keyword Arguments

Python also allows you to pass in keyword arguments. A keyword argument is specified by passing in a named argument, for example you might pass in age=10.

Let’s create a new example that shows a regular argument and a single keyword argument:

>>> def welcome(name: str, age: int=15) -> None:
...     print(f'Welcome {name}. You are {age} years old.')
... 
>>> welcome('Mike')
Welcome Mike. You are 15 years old.

This example has a regular argument, name and a keyword argument, age, which is defaulted to 15. When you call this code without specifying the age, you see that it defaults to 15.

To make things extra clear, here’s a different way you can call it:

>>> def welcome(name: str, age: int) -> None:
...     print(f'Welcome {name}. You are {age} years old.')
... 
>>> welcome(age=12, name='Mike')
Welcome Mike. You are 12 years old.

In this example, you specified both age and name parameters. When you do that, you can specify them in any order. For example, here you specified them in reverse order and Python still understood what you meant because you specified BOTH values.

Let’s see what happens when you don’t use keyword arguments:

>>> def welcome(name: str, age: int) -> None:
...     print(f'Welcome {name}. You are {age} years old.')
... 
>>> welcome(12, 'Mike')
Welcome 12. You are Mike years old.

When you pass in values without specifying where they should go, they will be passed in order. So name becomes 12 and age becomes 'Mike'.

Required and Default Arguments

Default arguments are a handy way to make your function callable with less arguments, whereas required arguments are ones that you have to pass in to the function for the function to execute.

Let’s look at an example that has one required argument and one default argument:

>>> def multiply(x: int, y: int=5) -> int:
...     return x * y
... 
>>> multiply(5)
25

The first argument, x is required. If you call multiply() without any arguments, you will receive an error:

>>> multiply()
Traceback (most recent call last):
  Python Shell, prompt 25, line 1
builtins.TypeError: multiply() missing 1 required positional argument: 'x'

The second argument y, is not required. In other words, it is a default argument where the default is 5. This allowed you to call multiply() with only one argument!

What are *args and **kwargs?

Most of the time, you will want your functions to only accept a small number of arguments, keyword arguments or both. You normally don’t want too many arguments as it becomes more complicated to change your function later.

However Python does support the concept of any number of arguments or keyword arguments.

You can use this special syntax in your functions:

  • *args – An arbitrary number of arguments
  • **kwargs – An arbitrary number of keyword arguments

The bit that you need to pay attention to is the * and the **. The name, arg or kwarg can be anything, but it is a convention to name them args and kwargs. In other words, most Python developers call them *args or **kwargs. While you aren’t forced to do so, you probably should so that the code is easy to recognize and understand.

Let’s look at an example:

>>> def any_args(*args):
...     print(args)
... 
>>> any_args(1, 2, 3)
(1, 2, 3)
>>> any_args(1, 2, 'Mike', 4)
(1, 2, 'Mike', 4)

Here you created any_args() which accepts any number of arguments including zero and prints them out.

You can actually create a function that has a required argument plus any number of additional arguments:

>>> def one_required_arg(required, *args):
...     print(f'{required=}')
...     print(args)
... 
>>> one_required_arg('Mike', 1, 2)
required='Mike'
(1, 2)

So in this example, your function’s first argument is required. If you were to call one_required_arg() without any arguments, you would get an error.

Now let’s try adding keyword arguments:

>>> def any_keyword_args(**kwargs):
...     print(kwargs)
... 
>>> any_keyword_args(1, 2, 3)
Traceback (most recent call last):
  Python Shell, prompt 7, line 1
builtins.TypeError: any_keyword_args() takes 0 positional arguments but 3 were given

Oops! You created the function to accept keyword arguments but only passed in normal arguments. This caused a TypeError to be thrown.

Let’s try passing in the same values as keyword arguments:

>>> def any_keyword_args(**kwargs):
...     print(kwargs)
... 
>>> any_keyword_args(one=1, two=2, three=3)
{'one': 1, 'two': 2, 'three': 3}

This time it worked the way you would expect it to.

Now let’s inspect our *args and **kwargs and see what they are:

>>> def arg_inspector(*args, **kwargs):
...     print(f'args are of type {type(args)}')
...     print(f'kwargs are of type {type(kwargs)}')
... 
>>> arg_inspector(1, 2, 3, x='test', y=5)
args are of type <class 'tuple'>
kwargs are of type <class 'dict'>

What this means is that args is a tuple and kwargs are a dict.

Let’s see if we can pass our function a tuple and dict for the *args and **kwargs:

>>> my_tuple = (1, 2, 3)
>>> my_dict = {'one': 1, 'two': 2}
>>> def output(*args, **kwargs):
...     print(f'{args=}')
...     print(f'{kwargs=}')
... 
>>> output(my_tuple)
args=((1, 2, 3),)
kwargs={}
>>> output(my_tuple, my_dict)
args=((1, 2, 3), {'one': 1, 'two': 2})
kwargs={}

Well that didn’t work quite right. Both the tuple and the dict ended up in the *args. Not only that, but the tuple stayed a tuple instead of being turned into three arguments.

You can make this work if you use a special syntax though:

>>> def output(*args, **kwargs):
...     print(f'{args=}')
...     print(f'{kwargs=}')
... 
>>> output(*my_tuple)
args=(1, 2, 3)
kwargs={}
>>> output(**my_dict)
args=()
kwargs={'one': 1, 'two': 2}
>>> output(*my_tuple, **my_dict)
args=(1, 2, 3)
kwargs={'one': 1, 'two': 2}

In this example, you call output() with *my_tuple. Python will extract the individual values in the tuple and pass each of them in as arguments. Next you passed in **my_dict, which tells Python to pass in each key/value pair as keyword arguments.

The final example passes in both the tuple and the dict.

Pretty neat!

Positional-only Parameters

Python 3.8 added a new feature to functions known as positional-only parameters. These use a special syntax to tell Python that some parameters have to be positional and some have to be keyword.

Let’s look at an example:

>>> def positional(name, age, /, a, b, *, key):
...     print(name, age, a, b, key)
... 
>>> positional(name='Mike')
Traceback (most recent call last):
  Python Shell, prompt 21, line 1
builtins.TypeError: positional() got some positional-only arguments passed as 
keyword arguments: 'name'

The first two parameters, name and age are positional-only. They can’t be passed in as keyword arguments, which is why you see the TypeError above. The arguments, a and b can be positional or keyword. Finally, key, is keyword-only.

The forward slash, /, indicates to Python that all arguments before the forward-slash as positional-only arguments. Anything following the forward slash are positional or keyword arguments up to th *. The asterisk indicates that everything following it as keyword-only arguments.

Here is a valid way to call the function:

>>> positional('Mike', 17, 2, b=3, keyword='test')
Mike 17 2 3 test

However, if you try to pass in only positional arguments, you will get an error:

>>> positional('Mike', 17, 2, 3, 'test')
Traceback (most recent call last):
  Python Shell, prompt 25, line 1
builtins.TypeError: positional() takes 4 positional arguments but 5 were given

The positional() function expects the last argument to be a keyword argument.

The main idea is that positional-only parameters allow the parameter name to change without breaking client code.

You may also use the same name for positional-only arguments and **kwargs:

>>> def positional(name, age, /, **kwargs):
...     print(f'{name=}')
...     print(f'{age=}')
...     print(f'{kwargs=}')
... 
>>> positional('Mike', 17, name='Mack')
name='Mike'
age=17
kwargs={'name': 'Mack'}

You can read about the full implementation and reasoning behind the syntax here:

Let’s move on and learn a little about the topic of scope!

Scope

All programming languages have the idea of scope. Scope tells the programming language what variables or functions are available to them.

Let’s look at an example:

>>> name = 'Mike'
>>> def welcome(name):
...     print(f'Welcome {name}')
... 
>>> welcome()
Traceback (most recent call last):
  Python Shell, prompt 34, line 1
builtins.TypeError: welcome() missing 1 required positional argument: 'name'
>>> welcome('Nick')
Welcome Nick
>>> name
'Mike'

The variable name is defined outside of the welcome() function. If you try to call welcome() without passing it an argument, it throws an error even though the argument matches the variable name. If you pass in a value to welcome(), that variable is only changed inside of the welcome() function. The name that you defined outside of the function remains unchanged.

Let’s look at an example where you define variables inside of functions:

>>> def add():
...     a = 2
...     b = 4
...     return a + b
... 
>>> def subtract():
...     a = 3
...     return a - b
... 
>>> add()
6
>>> subtract()
Traceback (most recent call last):
  Python Shell, prompt 40, line 1
  Python Shell, prompt 38, line 3
builtins.NameError: name 'b' is not defined

In add(), you define a and b and add them together. The variables, a and b have local scope. That means they can only be used within the add() function.

In subtract(), you only define a but try to use b. Python doesn’t check to see if b exists in the subtract() function until runtime.

What this means is that Python does not warn you that you are missing something here until you actually call the subtract() function. That is why you don’t see any errors until there at the end.

Python has a special global keyword that you can use to allow variables to be used across functions.

Let’s update the code and see how it works:

>>> def add():
...     global b
...     a = 2
...     b = 4
...     return a + b
... 
>>> def subtract():
...     a = 3
...     return a - b
... 
>>> add()
6
>>> subtract()
-1

This time you define b as global at the beginning of the add() function. This allows you to use b in subtract() even though you haven’t defined it there.

Globals are usually not recommended. It is easy to overlook them in large code files, which makes tracking down subtle errors difficult — for example, if you had called subtract() before you called add() you would still get the error, because even though b is global, it doesn’t exist until add() has been run.

In most cases where you would want to use a global, you can use a class instead.

There is nothing wrong with using globals as long as you understand what you are doing. They can be helpful at times. But you should use them with care.

Wrapping Up

Functions are a very useful way to reuse your code. They can be called repeatedly. Functions allow you to pass and receive data too.

In this tutorial, you learned about the following topics:

  • Creating a function
  • Calling a function
  • Passing arguments
  • Type hinting your arguments
  • Passing keyword arguments
  • Required and default arguments
  • *args and **kwargs
  • Positional-only arguments
  • Scope

You can use functions to keep your code clean and useful. A good function is self-contained and can be used easily by other functions. While it isn’t covered in this tutorial, you can nest functions inside of each other.

Related Articles