Now that we’ve learned about Python’s built-in functions, let’s see how we can create user-defined functions.

What is a function?

Functions are modular pieces of code. In other languages they are also known as subroutines, procedures, subprograms, callable units, etc.

Functions help to reduce redundancy and make code more flexible and easier to maintain. They are typically defined once, and used several times in one or more programs. Functions may be defined in one place (for example in an external module) and used somewhere else.

A function should not try to do many different things at once. Ideally, a function should do one thing well and nothing else. This makes it easier to reuse this function to solve a similar problem in another context.

Function basics

Working with functions involves two steps: definition and execution.

Defining a function

A function definition starts with the def statement, then the name of the function, a pair of parentheses, and a colon:

>>> def sayHello():
...     print('hello!')

The pair of parentheses in this example is empty, which means that this function takes no arguments. (More on that below.)

After the first line comes the body of the function, with the code that is executed every time the function is called. In this case, the function will print out hello!.

The body of a function cannot be completely empty. If we wish to create a function that does nothing at all, we need to place a pass statement in its body:

>>> def doNothing():
...     pass

Executing a function

Once a function has been defined, we can call it anywhere in our program, as many times as we want.

To call a function, we write its name followed by a pair of parentheses ():

>>> sayHello()
hello!

If we need to call this function multiple times, we can repeat it using a loop:

>>> for i in range(4):
...     sayHello()
hello!
hello!
hello!
hello!

The function object type

Functions are ‘first-class citizens’ in Python:

>>> type(doNothing)
<class 'function'>

Returning values

The sayHello function shown above does something: it prints a message. But it does not return anything:

>>> result = sayHello()
hello!
>>> print(result)
None

Functions that return a value

The next example shows a slightly different function which returns a string – using the return command – instead of printing it:

>>> def makeHello():
...     return 'hello!'

When we call this function, no message is printed – but a value is returned, which we can store in a variable for later use:

>>> txt = makeHello()
>>> txt
'hello!'

Of course, functions can also do both things at once: do something and return a value.

>>> def makeSpam():
...     print('making spam...')
...     return 'spam spam spam spam'
...
>>> mySpam = makeSpam()
making spam...
>>> mySpam
'spam spam spam spam'

Functions that return multiple values

Functions can also return more than one value (as a tuple):

>>> def makeColor():
...     r = 1.0
...     g = 0.5
...     b = 0.0
...     return r, g, b
...
>>> makeColor()
(1.0, 0.5, 0.0)

Function scope

Functions have their own private local scope. This means that variables defined inside a function are not accessible from outside the function:

>>> def myFunc():
...     a = 20
...     print(a)
...
>>> myFunc()
20
>>> a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined

Because the variable a was defined inside the function, it is not available to the global scope.

Variables defined in the global scope, however, are available to the function:

>>> b = 10
>>> def myFunc():
...     print(b)
...
>>> myFunc()
>>> 10

Function arguments

All function examples we’ve seen so far had no arguments – the functions did their job without any input from the ‘outside world’.

Arguments allow us to pass input values or parameters to the function, so the function can use them to do its work.

Here’s an example of a function that requires an argument:

>>> def sayHello(name):
...     print("hello", name)

If we try to call this function without an argument, an error will be raised:

>>> sayHello()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sayHello() missing 1 required positional argument: 'name'

The error message is clear about the problem: the function requires an argument, but we haven’t given any. Let’s try again:

>>> sayHello('Arthur')
hello Arthur
>>> sayHello('Maria')
hello Maria

Functions can take more than one argument. Here’s a function that requires three:

>>> def sayHello(firstName, lastName, title):
...     print("hello", title, firstName, lastName)
...
>>> sayHello('Arthur', 'Nudge', 'Dr.')
hello Dr. Arthur Nudge
sayHello('Marie', 'Curie', 'Mrs.')
hello Mrs. Marie Curie

The arguments must be passed to the function in the same order as in the function definition. This type of argument is known as positional argument.

Keyword arguments

Function arguments can also be defined using a keyword and a default value.

Here’s an example function with one required argument and two optional keyword arguments:

>>> def sayHello(firstName, lastName=None, daytime='morning'):
...     if lastName is not None:
...         print("good", daytime, firstName, lastName)
...     else:
...         print("good", daytime, firstName)
...

If we leave out a keyword argument when calling the function, its default value is used:

>>> sayHello('Jane')
good morning Jane
>>> sayHello('David', daytime='afternoon')
good afternoon David

Keyword arguments don’t have to be passed to the function in a particular order – but they must always come after positional arguments:

>>> sayHello('John', daytime='evening', lastName='Smith')
good evening John Smith

If we try to pass a keyword argument before positional arguments, we’ll get an error:

>>> sayHello(daytime='night', 'Alice')
Traceback (most recent call last):
  File "<untitled>", line 10
SyntaxError: positional argument follows keyword argument

Arbitrary arguments

Functions can also receive arbitrary arguments which are not known in advance, and which are not given an individual name in the function statement.

As an example, let’s start with a function to join two strings (first and last names):

>>> def fullName(firstName, lastName):
...     return ' '.join([firstName, lastName])
...
>>> fullName('Arthr', 'Nudge')

We can make this function more flexible by allowing it to support arbitrary arguments:

>>> def fullName(*args):
...     return ' '.join(args)
...
>>> fullName('Johann', 'Gambolputty', 'de von Ausfern-...', 'of Ulm')
'Johann Gambolputty de von Ausfern-... of Ulm'

It’s also possible to combine required and arbitrary arguments (in this order):

>>> def fullName(firstName, *lastNames):
...     return ' '.join([firstName, ' '.join(lastNames)])

Arbitrary keyword arguments

Arbitrary arguments can also be passed to the function with keywords, and can be accessed as a dictionary.

Here’s an example script that uses a function with arbitrary keyword arguments to set variable font info in a font:

def setFontInfo(font, **kwargs):
    for attr, value in kwargs.items():
        setattr(font.info, attr, value)

f = CurrentFont()

setFontInfo(f, familyName='MyTypeface', styleName='Bold', openTypeOS2WeightClass=700)

Order of function arguments

The different types of arguments must occur in a particular order when defining and calling functions:

  1. positional arguments
  2. arbitrary arguments (*args)
  3. keyword arguments
  4. arbitrary keyword arguments (**kwargs)
def myFunction(arg1, arg2, *args, key1="abc", key2="xyz", **kwargs):
    pass

Composition with functions

Functions can call other functions. Complex problems can be solved by combining small specialized functions.

>>> def formatCaption(pos):
...     x, y = pos
...     return f'x:{x} y:{y}'
...
>>> def drawCaption(pos):
...     text(formatCaption(pos), pos)
...
>>> drawCaption((180, 336))

Nested functions

Functions can be nested: a function can be defined inside another function. And since functions are ‘first class’ objects in Python, functions can also be created and destroyed dynamically, passed to other functions, returned as values, etc.

Here’s an example of a nested function. The outer function is a ‘factory’ function which creates a new function and returns it.

>>> def greetFactory(greeting):
...     def greet(name):
...         print(greeting, name)
...     return greet
...
>>> greet = greetFactory('hello')
>>> greet('Manuel')
hello Manuel

Last edited on 01/09/2021