Functions#

In this lesson you’ll learn about some slightly more advanced or obscure function writing.

Important

Be sure to complete the fundamentals Functions lesson first.

Table of Contents

Part 1: Optional arguments#

Part 1.1: Default Values#

You can make an argument optional by giving it a default value.

def hr(width=20):
    print("-" * width)

When you call the function, you can pass an argument as you normally would.

hr(10)
----------

Or skip the argument to use the default value.

hr()
--------------------

Any parameters without default values have to go before any with default values.

def hr(char, width=20):
    print(char * width)

Part 1.2: Exercise#

Exercise 88 (Defaults)

Define a function write() that takes three arguments:

  • message

  • before with a default of 0

  • after with a default of 0

The function should print message with the specified number of blank lines before and after, then print the string "----" at the end.

Call it with all variations of missing and present default arguments.

Part 1.3: Mutable Defaults#

Avoid using a mutable object as a default value, since it is evaluated only once when the function is defined.

def setup(project, options={}):
    if "title" not in options:
        options["title"] = project
    print(id(options), options)

Therefore any call that uses the default value will have the exact same object.

setup("home")
setup("work")
setup("school", {"title": "School Work"})
140534697403712 {'title': 'home'}
140534697403712 {'title': 'home'}
140534697399808 {'title': 'School Work'}

For mutable types, set the default value to None. Then in the body of the function, assign the value if the argument is falsy.

def setup(project, options=None):
    if not options:
        options = {}
    if "title" not in options:
        options["title"] = project
    print(id(options), options)

This will ensure that every time the function is called, a new value is created and assigned.

setup("home")
setup("work")
setup("school", {"title": "School Work"})
140534697429056 {'title': 'home'}
140534697404800 {'title': 'work'}
140534731659136 {'title': 'School Work'}

A shorthand for this is:

NAME = NAME or DEFAULT

def setup(project, options=None):
    options = options or {}
    if "title" not in options:
        options["title"] = project
    print(id(options), options)
setup("home")
setup("work")
setup("school", {"title": "School Work"})
140534731793408 {'title': 'home'}
140534731795200 {'title': 'work'}
140534697703744 {'title': 'School Work'}

Part 2: Arbitrary arguments#

Sometimes you want a function to be able to take an arbitrary arguments. These are called variadic arguments.

Part 2.1: Positional#

This can be done with a single asterisk before a parameter name. Any positional arguments will then be in a tuple with that name, in this case args.

def thing(*args):
    print(args)
    if args:
      print(args[0])

You can then call it with no arguments…

thing()
()

One argument…

thing("a")
('a',)
a

Or any number of arguments…

thing("a", "b", "c", "d", "e")
('a', 'b', 'c', 'd', 'e')
a

If you mix positional arguments and variadic the positional arguments must be first in the function definition.

def thing(stuff, *args):
    print(stuff, args)
    if args:
      print(args[0])

Arguments with default values go after *args.

def thing(stuff, *args, data=None):
    print(stuff, args, data)
    if args:
      print(args[0])

Note that when calling a function that like this you’ll need to use keyword arguments to specify the parameter name of any arguments with default values. If you send it as a positional argument, it will be part of args.

thing("abc", [1, 2, 3])
abc ([1, 2, 3],) None
[1, 2, 3]
thing("abc", data=[1, 2, 3])
abc () [1, 2, 3]

Part 2.1: Exercise#

Exercise 89 (Arbitrary Positional Arguments)

Modify your write() so that it takes an arbitrary number of positional arguments instead of message. It should converts all to strings then join them with a space between each before printing.

Example Usage

>>> write("abc", 123)
abc, 123
----

Part 2.2: Keyword#

To take arbitrary keyword arguments, put two asterisks before a parameter name.

def thing(**kwargs):
    print(kwargs)
    if "a" in kwargs:
        print(kwargs["a"])

The keyword arguments will then be in a dictionary with that name, in this case kwargs.

thing(a=1, b=2, c=3)
{'a': 1, 'b': 2, 'c': 3}
1

Variadic keyword arguments go after parameters with default values in the function definition.

def thing(stuff, *args, data=None, **kwargs):
    print(stuff, args, data, kwargs)
    if args:
      print(args[0])

Part 2.2: Exercise#

Exercise 90 (Arbitrary Keyword Arguments)

Modify write() to also take arbitrary keyword arguments. Each keyword argument should be formatted into a string: NAME=VALUE, then printed (along with any arguments) seperated by commas.

Example Usage

>>> write("abc", 123, a=1, b=2.0, c="three")
abc 123 a=1 b=2.0 c='three'
----

Part 3: Unpacking Arguments#

Sometimes want a function to take arbitrary arguments, sometimes you want to send all of the values in a list or dictionary as arguments to a function. This is known as unpacking.

Part 3.1: Sequences#

To send all elements in a list, tuple, or other sequence, put an asterisk before the object.

birth_stones = [
    "Garnet",
    "Amethyst",
    "Aquam",
    "Diamond",
    "Emerald"
]
print(*birth_stones)
Garnet Amethyst Aquam Diamond Emerald

You can unpack more than one sequence.

birth_stones = [
    "Garnet",
    "Amethyst",
    "Aquam",
    "Diamond",
    "Emerald"
]

flowers = (
    "Carnation",
    "Violet",
    "Jonquil",
    "Sweet Pea",
    "Lily"
)
print(*birth_stones, *flowers)
Garnet Amethyst Aquam Diamond Emerald Carnation Violet Jonquil Sweet Pea Lily

All unpacked sequences must go before any keyword arguments.

birth_stones = [
    "Garnet",
    "Amethyst",
    "Aquam",
    "Diamond",
    "Emerald"
]

flowers = (
    "Carnation",
    "Violet",
    "Jonquil",
    "Sweet Pea",
    "Lily"
)
print("Stones and flowers:", *birth_stones, *flowers, sep="\n")
Stones and flowers:
Garnet
Amethyst
Aquam
Diamond
Emerald
Carnation
Violet
Jonquil
Sweet Pea
Lily

Part 3.2: Exercise#

Exercise 91 (Unpacking Sequences)

Make a list of cities you’ve lived in. Print them by unpacking the list as arguments to the print() function. Bonus: Mix with positional and keyword arguments.

Part 3.3: Mappings#

To send all elements of a dictionary as keyword arguments, put two asterisks before the dictionary.

def show(red, green, blue, hex_code):
    print(f"Hex Color: #{hex_code} is RGB: ({red}, {green}, {blue})")

color = {
  "hex_code": "21abcd",
  "red": 33,
  "green": 171,
  "blue": 205,
}

show(**color)
Hex Color: #21abcd is RGB: (33, 171, 205)

As with unpacking sequences, you can unpack multiple dictionaries.

color = {
  "hex_code": "21abcd",
}

rgb = {
  "red": 33,
  "green": 171,
  "blue": 205,
}

show(**color, **rgb)
Hex Color: #21abcd is RGB: (33, 171, 205)

The unpacked keyword arguments must go after any positional arguments.

color = {
    "green": 0,
    "blue": 0,
}

show(255, **color, hex_code="#FF0000")
Hex Color: ##FF0000 is RGB: (255, 0, 0)

Part 3.4: Exercise#

Exercise 92 (Unpacking Mappings)

Make a dictionary called options that includes some of the values for sep, end or file. Unpack options it as arguments to print() after printing your previous list of cities.

Part 4: Annotations#

Python provides a syntax for documenting the type of various values called annotations. While annotations have no effect on how a function behaves, it can be helpful in clarifying the intended usage.

Part 4.1: Parameter Hints#

You can specify the expected type of each parameter by adding a colon after the parameter name, followed by the type.

def debug(message: str):
    print(message)

Part 4.2: Variable Hints#

Incidentally, you can specify the type of a particular variable the same way.

maximum: int = 100
name: str

Part 4.3: Return hints#

You can specify the type of any returned value by adding -> followed by the type before the colon.

from random import randint

def random(limit: int=10) -> int:
    return randint(1, limit)

Be aware that is strictly documentation. An annotation does not change or enforce the type of a given value.

Part 4.4: Exercise#

Exercise 93 (Annotations)

Write a function get_keys() that takes two arguments: source (dict) and keys (list). It should return a dictionary containing all key value pairs from source for keys in keys.

Annotate both the parameters and return value.

** Example Usage**

1>>> import string
2>>> alpha = dict(zip(string.ascii_lowercase, range(1, 27)))
3
4>>> get_keys(alpha, ["x", "y", "x"])
5{'x': 24, 'y': 25, 'z': 26}

Part 5: Lambdas#

Python provides an inline syntax for short functions that return the results of a single expression called lambdas.

def random():
    return randint(1, 100)
random = lambda: randint(1, 100)
def random(limit):
    return randint(1, limit)
random = lambda limit: randint(1, limit)

Part 6: Shorthand#

It is possible, though not recommended, to write a function (or any other compound statement) all on one line, assuming that it contains a single line.

The following two functions are equivalent.

1def hello():
2  print("hello")
1def hello(): print("hello")

Reference#

Glossary#

Functions#

variadic#
variadic arguments#

When any number of arguments may be passed to a function or method.

annotations#
type hints#

Syntax for specifying the type of a variable, class attribute, function parameter or return value.

lambda#

An anonymous inline function consisting of a single expression.

unpacking#
unpacking arguments#

Sending all elements in a collection as arguments to a function call.

See Also#