functions

what is a function?

A function is a unit or block of code that is callable. This means I can write statements that I can use to do something at a different time from when I write them. They can make code smaller, easier to read, test, reuse, maintain and improve.

Computer Programming involves providing a process with input data and the process returning output data, for example

input_data -> process -> output_data

I think of it like the concept of mapping a function f with inputs x and an output of y

\[f(x) -> y\]

in other words

function(input_data) -> output_data

the function processes input_data and returns output_data as the result.

In Python functions are made with the def keyword, a name, parentheses and a colon at the end

def name_of_function():

preview

Here are the tests I have by the end of the chapter

 1import src.functions
 2import unittest
 3
 4
 5class TestFunctions(unittest.TestCase):
 6
 7    def test_making_a_function_w_pass(self):
 8        self.assertIsNone(src.functions.w_pass())
 9
10    def test_making_a_function_w_return(self):
11        self.assertIsNone(src.functions.w_return())
12
13    def test_making_a_function_w_return_none(self):
14        self.assertIsNone(src.functions.w_return_none())
15
16    def test_constant_function(self):
17        self.assertEqual(
18            src.functions.constant(),
19            'the same thing'
20        )
21
22    def test_identity_function(self):
23        self.assertIsNone(src.functions.identity(None))
24        self.assertEqual(src.functions.identity(object), object)
25
26    def test_functions_w_positional_arguments(self):
27        self.assertEqual(
28            src.functions.w_positional_arguments('first', 'last'),
29            ('first', 'last')
30        )
31        self.assertEqual(
32            src.functions.w_positional_arguments('last', 'first'),
33            ('last', 'first')
34        )
35
36    def test_functions_w_keyword_arguments(self):
37        self.assertEqual(
38            src.functions.w_keyword_arguments(
39                first='first', last='last',
40            ),
41            ('first', 'last')
42        )
43        self.assertEqual(
44            src.functions.w_keyword_arguments(
45                last='last', first='first',
46            ),
47            ('first', 'last')
48        )
49        self.assertEqual(
50            src.functions.w_keyword_arguments('last', 'first'),
51            ('last', 'first')
52        )
53
54    def test_functions_w_positional_and_keyword_arguments(self):
55        self.assertEqual(
56            src.functions.w_positional_and_keyword_arguments(
57                'first', last='last',
58            ),
59            ('first', 'last')
60        )
61
62    def test_functions_w_default_arguments(self):
63        self.assertEqual(
64            src.functions.w_default_arguments('jane'),
65            ('jane', 'doe')
66        )
67        self.assertEqual(
68            src.functions.w_default_arguments('joe', 'blow'),
69            ('joe', 'blow')
70        )
71
72    def test_functions_w_unknown_arguments(self):
73        self.assertEqual(
74            src.functions.w_unknown_arguments(
75                0, 1, 2, 3, a=4, b=5, c=6, d=7,
76            ),
77            ((0, 1, 2, 3), {'a': 4, 'b': 5, 'c': 6, 'd': 7})
78        )
79        self.assertEqual(
80            src.functions.w_unknown_arguments(0, 1, 2, 3),
81            ((0, 1, 2, 3), {})
82        )
83        self.assertEqual(
84            src.functions.w_unknown_arguments(a=4, b=5, c=6, d=7),
85            ((), dict(a=4, b=5, c=6, d=7))
86        )
87        self.assertEqual(
88            src.functions.w_unknown_arguments(),
89            ((), {})
90        )
91
92
93# Exceptions seen
94# AssertionError
95# NameError
96# IndentationError
97# SyntaxError

questions about functions

Here are the questions you can answer after going through this chapter


start the project

  • I name this project functions

  • I open a terminal

  • then I make a directory for the project

    mkdir functions
    

    the terminal goes back to the command line

    .../pumping_python
    
  • I change directory to the project

    cd functions
    

    the terminal shows I am now in the functions folder

    .../pumping_python/functions
    
  • I make a folder for the source code

    mkdir src
    

    the terminal goes back to the command line

    .../pumping_python/functions
    
  • I use touch to make an empty file for the program in the src folder

    touch src/functions.py
    

    on Windows without Windows Subsystem for Linux use New-Item src/functions.py instead of touch src/functions.py

    New-Item src/functions.py
    

    the terminal goes back to the command line

    .../pumping_python/functions
    
  • I make a directory for the tests

    mkdir tests
    

    the terminal goes back to the command line

  • I use touch to make an empty file in the tests folder to tell Python that it is a Python package

    Attention

    use 2 underscores (__) before and after init for __init__.py not _init_.py

    touch tests/__init__.py
    

    on Windows without Windows Subsystem for Linux use New-Item tests/__init__.py instead of touch tests/__init__.py

    New-Item tests/__init__.py
    

    the terminal goes back to the command line

  • I make an empty file for the actual test

    touch tests/test_functions.py
    

    on Windows without Windows Subsystem for Linux use New-Item tests/test_functions.py instead of touch tests/test_functions.py

    New-Item tests/test_functions.py
    

    the terminal goes back to the command line

  • I open test_functions.py in the editor of the Integrated Development Environment (IDE)

    Tip

    I can open a file from the terminal in Visual Studio Code by typing code and the name of the file, for example, when I type this

    code tests/test_functions.py
    

    test_functions.py opens up in the editor

  • I add the first failing test to test_functions.py

    1import unittest
    2
    3
    4class TestFunctions(unittest.TestCase):
    5
    6    def test_failure(self):
    7        self.assertFalse(True)
    
  • I make a virtual environment in the terminal

    python3 -m venv .venv
    

    on Windows without Windows Subsystem for Linux use python3 -m venv .venv instead of python3 -m venv .venv

    python -m venv .venv
    

    the terminal takes some time then goes back to the command line

  • I activate the virtual environment

    source .venv/bin/activate
    

    on Windows without Windows Subsystem for Linux use .venv/bin/activate.ps1 instead of source .venv/bin/activate

    .venv/scripts/activate.ps1
    

    the terminal shows

    (.venv) .../pumping_python/functions
    
  • I upgrade the Python package manager (pip) to the latest version

    python3 -m pip install --upgrade pip
    

    the terminal shows pip being uninstalled then installs the latest version or shows that it is already the latest version

  • I make a requirements.txt file for the Python programs my project needs

    echo "pytest-watch" > requirements.txt
    

    the terminal goes back to the command line

  • I use pip to use the requirements file to install pytest-watch

    python3 -m pip install --requirement requirements.txt
    

    on Windows without Windows Subsystem for Linux use python -m pip install --requirement requirements.txt instead of python3 -m pip install --requirement requirements.txt

    python -m pip install --requirement requirements.txt
    

    the terminal shows pip downloads and installs the Python programs that pytest-watch needs to run

  • I use pytest-watch to run the test

    pytest-watch
    

    the terminal shows

    ================================ FAILURES ================================
    _______________________ TestFunctions.test_failure _______________________
    
    self = <tests.test_functions.TestFunctions testMethod=test_failure>
    
        def test_failure(self):
    >       self.assertFalse(True)
    E       Functions: True is not false
    
    tests/test_functions.py:7: AssertionError
    ======================== short test summary info =========================
    FAILED tests/test_functions.py::TestFunctions::test_failure - Functions: True is not false
    =========================== 1 failed in X.YZs ============================
    
  • I hold ctrl (Windows/Linux) or option or command (MacOS) on the keyboard and use the mouse to click on tests/test_functions.py:7 to open it in the editor

  • I add AssertionError to the list of Exceptions seen in test_functions.py

     7        self.assertFalse(True)
     8
     9
    10# Exceptions seen
    11# AssertionError
    
  • then I change True to False in the assertion

    7        self.assertFalse(False)
    

    the test passes


test_making_a_function_w_pass

I can make a function with the pass keyword

RED: make it fail

  • I change test_failure to test_making_a_function_w_pass

     1import unittest
     2
     3
     4class TestFunctions(unittest.TestCase):
     5
     6    def test_making_a_function_w_pass(self):
     7        self.assertIsNone(src.functions.w_pass())
     8
     9
    10# Exceptions seen
    

    the terminal shows NameError

    NameError: name 'src' is not defined
    
  • I add it to the list of Exceptions seen in test_functions.py

    10# Exceptions seen
    11# AssertionError
    12# NameError
    

GREEN: make it pass

  • I add an import statement at the top of the file

    1import src.functions
    2import unittest
    3
    4
    5class TestFunctions(unittest.TestCase):
    

    the terminal shows AttributeError

    AttributeError: module 'src.functions' has no attribute 'w_pass'
    

    functions.py in the src folder does not have anything named w_pass inside it

  • I add to the list of Exceptions seen

    11# Exceptions seen
    12# AssertionError
    13# ModuleNotFoundError
    14# AttributeError
    
  • I open functions.py from the src folder in the editor, then I add a function definition

    1def w_pass():
    2    pass
    

    the test passes

    • the test checks if the result of the call to src.functions.w_pass is None

    • the function definition simply says pass and the test passes

    • pass is a placeholder keyword which allows the function definition to follow Python language rules

    • the test passes because all functions return None by default, as if the function has an invisible line that says return None, which leads me to the next test


test_making_a_function_w_return

I can make a function with a return statement

RED: make it fail

I add a new failing test in test_functions.py

 7    def test_making_a_function_w_pass(self):
 8        self.assertIsNone(src.functions.w_pass())
 9
10    def test_making_a_function_w_return(self):
11        self.assertIsNone(src.functions.w_return())
12
13
14# Exceptions seen

the terminal shows AttributeError

AttributeError: module 'src.functions' has no attribute 'w_return'

functions.py in the src folder does not have w_return in it

GREEN: make it pass

I add a new function with pass to functions.py

1def w_pass():
2    pass
3
4
5def w_return():
6    pass

the test passes

REFACTOR: make it better

I change pass to a return statement

5def w_return():
6    return

the test is still green.

I have 2 functions with different statements in their body but they both return None, because “all functions return None by default, as if the function has an invisible line that says return None”, which leads me to the next test


test_making_a_function_w_return_none

I can make a function with a return statement that says what the function returns

RED: make it fail

I add another failing test to test_functions.py

10    def test_making_a_function_w_return(self):
11        self.assertIsNone(src.functions.w_return())
12
13    def test_making_a_function_w_return_none(self):
14        self.assertIsNone(src.functions.w_return_none())
15
16
17# Exceptions seen

the terminal shows AttributeError

AttributeError: module 'src.functions' has no attribute 'w_return_none'

w_return_none is not defined in functions.py in the src folder

GREEN: make it pass

I add a function definition to functions.py

 5def w_return():
 6    return
 7
 8
 9def w_return_none():
10    return

the test passes

REFACTOR: make it better

I add None to the return statement

 9def w_return_none():
10    return None

I like to write my functions this way, saying exactly what it returns, that way anyone can tell what the function returns without knowing what it does or even understanding Python code


test_constant_function

constant functions always return the same thing when they are called

RED: make it fail

I add a test to test_functions.py

13    def test_making_a_function_w_return_none(self):
14        self.assertIsNone(src.functions.w_return_none())
15
16    def test_constant_function(self):
17        self.assertEqual(
18            src.functions.constant(),
19            'the same thing'
20        )
21
22
23# Exceptions seen

the terminal shows AttributeError

AttributeError: module 'src.functions' has no attribute 'constant'

I have not added a definition for constant in functions.py in the src folder

GREEN: make it pass

I add the function to functions.py

 9def w_return_none():
10    return None
11
12
13def constant():
14    return None

the terminal shows AssertionError

AssertionError: None != 'the same thing'

what the constant function returns and what the test expects are different. I change the return statement to make them match

13def constant():
14    return 'the same thing'

the test passes.

A constant function always return the same thing when called, I can use them in place of variables, though the number of cases where they are faster than variables is pretty small. It is something like if the function is called less than 10 times, but who’s counting?


test_identity_function

The identity function returns its input as output, it’s also in the Truth Table chapter in test_logical_identity

RED: make it fail

I add a failing test in test_functions.py

16    def test_constant_function(self):
17        self.assertEqual(
18            src.functions.constant(),
19            'the same thing'
20        )
21
22    def test_identity_function(self):
23        self.assertIsNone(src.functions.identity(None))
24
25
26# Exceptions seen

the terminal shows AttributeError

AttributeError: module 'src.functions' has no attribute 'identity'

GREEN: make it pass

  • I add a function to functions.py

    13def constant():
    14    return 'the same thing'
    15
    16
    17def identity():
    18    return None
    

    the terminal shows TypeError

    TypeError: identity() takes 0 positional arguments but 1 was given
    

    the definition for identity does not allow inputs and the test sends None as input

  • I add the error to the list of Exceptions seen in test_functions.py

    26# Exceptions seen
    27# AssertionError
    28# NameError
    29# AttributeError
    30# TypeError
    
  • I add a name in parentheses for the identity function to take input in functions.py

    17def identity(the_input):
    18    return None
    

    the test passes. I am genius

REFACTOR: make it better

The requirement for the identity function is that it returns the same thing it is given, the test is currently passing when None is given as input. Does it pass when another value is given or does it always return None? Time to write a test

  • I add a new assertion to test_identity_function in test_functions.py

    22def test_identity_function(self):
    23    self.assertIsNone(src.functions.identity(None))
    24    self.assertEqual(src.functions.identity(object), object)
    

    the terminal shows AssertionError

    AssertionError: None != <class 'object'>
    

    the function returns None instead of <class 'object'> in the second case, I am not all the way genius, yet

  • I change the return statement of identity in functions.py to match the expectation

    17def identity(the_input):
    18    return the_input
    

    the test passes

I sometimes use the Identity Function when I am testing connections to see if my test is connected to what I am testing. If I can send input and received the same input back then I can start making changes to see what results I get.


The Identity Function takes one input, the following tests are for functions that take more than one.


test_functions_w_positional_arguments

RED: make it fail

I add a failing test to test_functions.py

22    def test_identity_function(self):
23        self.assertIsNone(src.functions.identity(None))
24        self.assertEqual(src.functions.identity(object), object)
25
26    def test_functions_w_positional_arguments(self):
27        self.assertEqual(
28            src.functions.w_positional_arguments('first', 'last'),
29            ('first', 'last')
30        )
31
32
33# Exceptions seen

the terminal shows AttributeError

AttributeError: module 'src.functions' has no attribute 'w_positional_arguments'

GREEN: make it pass

  • I add a function to functions.py

    17def identity(the_input):
    18    return the_input
    19
    20
    21def w_positional_arguments():
    22    return None
    

    the terminal shows TypeError

    TypeError: w_positional_arguments() takes 0 positional arguments but 2 were given
    
  • I make the function take input by adding a name in parentheses

    21def w_positional_arguments(first_input):
    22    return None
    

    the terminal shows TypeError

    TypeError: w_positional_arguments() takes 1 positional argument but 2 were given
    
  • I make w_positional_arguments take another input by adding another name in parentheses

    21def w_positional_arguments(first_input, last_input):
    22    return None
    

    the terminal shows AssertionError

    AssertionError: None != ('first', 'last')
    
  • I change the return statement

    21def w_positional_arguments(first_input, last_input):
    22    return first_input, last_input
    

    the test passes

REFACTOR: make it better

  • The problem with giving arguments this way is that they have to be in the order the function expects or I get a different behavior. I add a test to test_functions.py to show this

    26    def test_functions_w_positional_arguments(self):
    27        self.assertEqual(
    28            src.functions.w_positional_arguments('first', 'last'),
    29            ('first', 'last')
    30        )
    31        self.assertEqual(
    32            src.functions.w_positional_arguments('last', 'first'),
    33            ('first', 'last')
    34        )
    35
    36
    37# Exceptions seen
    

    the terminal shows AssertionError

    AssertionError: Tuples differ: ('last', 'first') != ('first', 'last')
    
  • I change the expectation of the test in test_functions.py

    31      self.assertEqual(
    32          src.functions.w_positional_arguments('last', 'first'),
    33          ('last', 'first')
    34      )
    

    the test passes.

The order matters when passing positional arguments to a function, because they are processed based on the position or in the order they are given to the function


test_functions_w_keyword_arguments

There is a problem with using positional arguments, the inputs must always be supplied in the right order, which means the function behaves in an unexpected way when it gets input out of order.

I can use Keyword Arguments to make sure it behaves how I want even when I send input out of order

RED: make it fail

I add a new test to test_functions.py

31        self.assertEqual(
32            src.functions.w_positional_arguments('last', 'first'),
33            ('last', 'first')
34        )
35
36    def test_functions_w_keyword_arguments(self):
37        self.assertEqual(
38            src.functions.w_keyword_arguments(
39                first_input='first', last_input='last',
40            ),
41            ('first', 'last')
42        )
43
44
45# Exceptions seen

the terminal shows AttributeError

AttributeError: module 'src.functions' has no attribute 'w_keyword_arguments'

functions.py in the src folder is missing a definition for w_keyword_arguments

GREEN: make it pass

  • I add a function definition to functions.py

    21def w_positional_arguments(first_input, last_input):
    22    return first_input, last_input
    23
    24
    25def w_keyword_arguments():
    26    return None
    

    the terminal shows TypeError

    TypeError: w_keyword_arguments() got an unexpected keyword argument 'first_input'
    
  • I add the name for the argument in parentheses

    25def w_keyword_arguments(first_input):
    26    return None
    

    the terminal shows TypeError

    TypeError: w_keyword_arguments() got an unexpected keyword argument 'last_input'. Did you mean
    
  • I add the name in parentheses

    25def w_keyword_arguments(first_input, last_input):
    26    return None
    

    the terminal shows AssertionError

    AssertionError: None != ('first', 'last')
    

    I change the return statement

    25def w_keyword_arguments(first_input, last_input):
    26    return first_input, last_input
    

    the test passes

w_keyword_arguments and w_positional_arguments are the same functions. The only difference in the definitions is their names. The difference that matters in the tests is in how I call the functions.

In the first case I use positional arguments which have to be given in order

w_positional_arguments('first', 'last')
w_positional_arguments('last', 'first')

in the second case I use keyword arguments which use the names of the variables in parentheses in the function definition when calling the it

w_keyword_arguments(first_input='first', last_input='last')

REFACTOR: make it better

  • I add another test with the keyword arguments given out of order in test_functions.py

    36    def test_functions_w_keyword_arguments(self):
    37        self.assertEqual(
    38            src.functions.w_keyword_arguments(
    39                first_input='first', last_input='last',
    40            ),
    41            ('first', 'last')
    42        )
    43        self.assertEqual(
    44            src.functions.w_keyword_arguments(
    45                last_input='last', first_input='first',
    46            ),
    47            ('last', 'first')
    48        )
    

    the terminal shows AssertionError

    AssertionError: Tuples differ: ('first', 'last') != ('last', 'first')
    

    the order stayed the same

  • I change the expectation to match

    43        self.assertEqual(
    44            src.functions.w_keyword_arguments(
    45                last_input='last', first_input='first',
    46            ),
    47            ('first', 'last')
    48        )
    

    the test passes. Keyword Arguments allow the input to be passed in any order

  • I can still call the function without using the names, the same way I did in test_functions_w_positional_arguments. I add an assertion to show this

    43      self.assertEqual(
    44          src.functions.w_keyword_arguments(
    45              last_input='last', first_input='first',
    46          ),
    47          ('first', 'last')
    48      )
    49      self.assertEqual(
    50          src.functions.w_keyword_arguments('last', 'first'),
    51          ('first', 'last')
    52      )
    

    the terminal shows AssertionError

    AssertionError: Tuples differ: ('last', 'first') != ('first', 'last')
    

    I change the expectation to match

    49      self.assertEqual(
    50          src.functions.w_keyword_arguments('last', 'first'),
    51          ('last', 'first')
    52      )
    53
    54
    55# Exceptions seen
    

    the test passes

Positional Arguments MUST be given in the expected order, Keyword Arguments can be given in any order


test_functions_w_positional_and_keyword_arguments

I can write functions that take both positional and keyword arguments<test_functions_w_keyword_arguments>

RED: make it fail

I add a failing test to test_functions.py

49        self.assertEqual(
50            src.functions.w_keyword_arguments('last', 'first'),
51            ('last', 'first')
52        )
53
54    def test_functions_w_positional_and_keyword_arguments(self):
55        self.assertEqual(
56            src.functions.w_positional_and_keyword_arguments(
57                last_input='last', 'first',
58            ),
59            ('first', 'last')
60        )
61
62
63# Exceptions seen

the terminal shows SyntaxError

SyntaxError: positional argument follows keyword argument

I cannot put a keyword argument before a positional argument in Python

GREEN: make it pass

  • I change the order of the arguments to follow Python rules

    54    def test_functions_w_positional_and_keyword_arguments(self):
    55        self.assertEqual(
    56            src.functions.w_positional_and_keyword_arguments(
    57                'first', last_input='last',
    58            ),
    59            ('first', 'last')
    60        )
    

    the terminal shows AttributeError

    AttributeError: module 'src.functions' has no attribute 'w_positional_and_keyword_arguments'
    
  • I add a function to functions.py

    25  def w_keyword_arguments(first_input, last_input):
    26      return first_input, last_input
    27
    28
    29  def w_positional_and_keyword_arguments():
    30      return None
    

    the terminal shows TypeError

    TypeError: w_positional_and_keyword_arguments() got an unexpected keyword argument 'last_input'
    
  • I add the name to the function definition in parentheses in functions.py

    29def w_positional_and_keyword_arguments(last_input):
    30    return None
    

    the terminal shows

    TypeError: w_positional_and_keyword_arguments() got multiple values for argument 'last_input'
    
  • I add another name in parentheses

    29def w_positional_and_keyword_arguments(last_input, first_input):
    30    return None
    

    the terminal shows TypeError

    TypeError: w_positional_and_keyword_arguments() got multiple values for argument 'last_input'
    

    Python cannot tell the difference between the 2 values since last_input is both the second positional argument and passed in as a keyword argument

  • I change the order of the names in parentheses

    29def w_positional_and_keyword_arguments(first_input, last_input):
    30    return None
    

    the terminal shows AssertionError

    AssertionError: None != ('first', 'last')
    

    I cannot put positional arguments after keyword arguments

  • I change the return statement

    29def w_positional_and_keyword_arguments(first_input, last_input):
    30    return first_input, last_input
    

    the test passes.

There is no difference between the last 3 functions except their names, they all have this pattern

def a_name(first_input, last_input):
    return first_input, last_input

what is different is the way I called them in the tests

w_positional_arguments('first', 'last')
w_positional_arguments('first', 'last')
w_keyword_arguments(first_input='first', last_input='last')
w_keyword_arguments(last_input='last', first_input='first')
w_keyword_arguments('last', 'first')
w_positional_and_keyword_arguments('first', last_input='last')

test_functions_w_default_arguments

I can use positional and keyword arguments when I want a function to take inputs that are needed and inputs that are NOT needed

RED: make it fail

I add a failing test to test_functions.py

54    def test_functions_w_positional_and_keyword_arguments(self):
55        self.assertEqual(
56            src.functions.w_positional_and_keyword_arguments(
57                'first', last_input='last',
58            ),
59            ('first', 'last')
60        )
61
62    def test_functions_w_default_arguments(self):
63        self.assertEqual(
64            src.functions.w_default_arguments('jane', last_name='doe'),
65            ('jane', 'doe')
66        )
67
68
69# Exceptions seen

the terminal shows AttributeError

AttributeError: module 'src.functions' has no attribute 'w_default_arguments'. Did you mean: 'w_keyword_arguments'?

GREEN: make it pass

I add a function to functions.py

29def w_positional_and_keyword_arguments(first_input, last_input):
30    return first_input, last_input
31
32
33def w_default_arguments(first_name, last_name):
34    return first_name, last_name

the test passes

REFACTOR: make it better

  • I remove last_name='doe' from the call to w_default_arguments in test_functions.py

    62    def test_functions_w_default_arguments(self):
    63        self.assertEqual(
    64            src.functions.w_default_arguments('jane'),
    65            ('jane', 'doe')
    66        )
    

    the terminal shows TypeError

    TypeError: w_default_arguments() missing 1 required positional argument: 'last_name'
    

    the last_name argument is needed when the function is called

  • I make the argument a choice by giving it a default value in functions.py

    33def w_default_arguments(first_name, last_name='doe'):
    34    return first_name, last_name
    

    the test passes.

  • Calling the function without the last_name argument

    w_default_arguments('jane')
    

    is now the same as calling it with the default value

    w_default_arguments('jane', last_name='doe')
    

    I add another assertion to test_functions.py to show that I can still call the function with different values

    62    def test_functions_w_default_arguments(self):
    63        self.assertEqual(
    64            src.functions.w_default_arguments('jane'),
    65            ('jane', 'doe')
    66        )
    67        self.assertEqual(
    68            src.functions.w_default_arguments('joe', 'blow'),
    69            ()
    70        )
    71
    72
    73# Exceptions seen
    

    the terminal shows AssertionError

    AssertionError: Tuples differ: ('joe', 'blow') != ()
    

    I change the expectation to match

    67        self.assertEqual(
    68            src.functions.w_default_arguments('joe', 'blow'),
    69            ('joe', 'blow')
    70        )
    

    the test passes


test_functions_w_unknown_arguments

I can make functions that take any number of positional and keyword arguments. This means I do not need to know how many inputs are sent to the function when it is called

RED: make it fail

I add a new test to test_functions.py

67        self.assertEqual(
68            src.functions.w_default_arguments('joe', 'blow'),
69            ('joe', 'blow')
70        )
71
72    def test_functions_w_unknown_arguments(self):
73        self.assertEqual(
74            src.functions.w_unknown_arguments(
75                0, 1, 2, 3, a=4, b=5, c=6, d=7,
76            ),
77            None
78        )
79
80
81# Exceptions seen

the terminal shows AttributeError

AttributeError: module 'src.functions' has no attribute 'w_unknown_arguments'. Did you mean: 'w_keyword_arguments'?

GREEN: make it pass

  • I add a function to functions.py

    33def w_default_arguments(first_name, last_name='doe'):
    34    return first_name, last_name
    35
    36
    37def w_unknown_arguments():
    38    return None
    

    the terminal shows TypeError

    TypeError: w_unknown_arguments() got an unexpected keyword argument 'a'
    
  • I add the name to the function definition

    37def w_unknown_arguments(a):
    38    return None
    

    the terminal shows TypeError

    TypeError: w_unknown_arguments() got multiple values for argument 'a'
    

    I had this same problem in test_functions_w_positional_and_keyword_arguments, Python cannot tell which arguments are positional or keyword arguments yet

  • Python has a way to allow passing any number of keyword arguments without knowing how many they are. I use it to replace a in the parentheses

    37def w_unknown_arguments(**kwargs):
    38    return None
    

    the terminal shows TypeError

    TypeError: w_unknown_arguments() takes 0 positional arguments but 4 were given
    
  • I add a name for the first positional argument

    37def w_unknown_arguments(**kwargs, x):
    38    return None
    

    the terminal shows SyntaxError

    SyntaxError: arguments cannot follow var-keyword argument
    

    a reminder that I cannot put positional arguments after keyword arguments

  • I add SyntaxError to the list of Exceptions seen in test_functions.py

    81# Exceptions seen
    82# AssertionError
    83# NameError
    84# AttributeError
    85# TypeError
    86# SyntaxError
    
  • I change the order of the inputs in w_unknown_arguments in functions.py

    37def w_unknown_arguments(x, **kwargs):
    38    return None
    

    the terminal shows TypeError

    TypeError: w_unknown_arguments() takes 1 positional argument but 4 were given
    
  • I can add names for the other positional arguments, or I can do something like what I did with the keyword arguments

    37def w_unknown_arguments(*args, **kwargs):
    38    return None
    

    the test passes

REFACTOR: make it better

  • *args, **kwargs is Python convention. I change the names to be clearer

    37def w_unknown_arguments(*arguments, **keyword_arguments):
    38    return None
    

    the test is still green

  • I want the function to return its input, remember the identity function, I change the return statement

    37def w_unknown_arguments(*arguments, **keyword_arguments):
    38    return arguments, keyword_arguments
    

    the terminal shows

    AssertionError: ((0, 1, 2, 3), {'a': 4, 'b': 5, 'c': 6, 'd': 7}) != None
    
  • I copy the tuple from the terminal and use it to change the expectation in test_functions_w_unknown_arguments in test_functions.py

    72    def test_functions_w_unknown_arguments(self):
    73        self.assertEqual(
    74            src.functions.w_unknown_arguments(
    75                0, 1, 2, 3, a=4, b=5, c=6, d=7,
    76            ),
    77            ((0, 1, 2, 3), {'a': 4, 'b': 5, 'c': 6, 'd': 7})
    78        )
    

    the test passes

how Python reads positional arguments

I want to see what happens when I call the function with ONLY positional arguments. I add a new assertion

73        self.assertEqual(
74            src.functions.w_unknown_arguments(
75                0, 1, 2, 3, a=4, b=5, c=6, d=7,
76            ),
77            ((0, 1, 2, 3), {'a': 4, 'b': 5, 'c': 6, 'd': 7})
78        )
79        self.assertEqual(
80            src.functions.w_unknown_arguments(0, 1, 2, 3),
81            ()
82        )

the terminal shows AssertionError

AssertionError: Tuples differ: ((0, 1, 2, 3), {}) != ()

I change the expectation to match

79        self.assertEqual(
80            src.functions.w_unknown_arguments(0, 1, 2, 3),
81            ((0, 1, 2, 3), {})
82        )
83
84
85# Exceptions seen

the test passes. The function reads the positional arguments as a tuple (things in parentheses (()) separated by commas)

how Python reads keyword arguments

I add another assertion to see what happens when I call the function with ONLY keyword arguments

79        self.assertEqual(
80            src.functions.w_unknown_arguments(0, 1, 2, 3),
81            ((0, 1, 2, 3), {})
82        )
83        self.assertEqual(
84            src.functions.w_unknown_arguments(a=4, b=5, c=6, d=7),
85            ()
86        )

the terminal shows

AssertionError: Tuples differ: ((), {'a': 4, 'b': 5, 'c': 6, 'd': 7}) != ()

I change the expectation to match

83        self.assertEqual(
84            src.functions.w_unknown_arguments(a=4, b=5, c=6, d=7),
85            ((), dict(a=4, b=5, c=6, d=7))
86        )
87
88
89# Exceptions seen

the test passes. The function reads the keyword arguments as a dictionary (key-value pairs in curly braces ({}) separated by commas)


  • I add one more assertion to see what happens when I call the function with no inputs

    83      self.assertEqual(
    84          src.functions.w_unknown_arguments(a=4, b=5, c=6, d=7),
    85          ((), dict(a=4, b=5, c=6, d=7))
    86      )
    87      self.assertEqual(
    88          src.functions.w_unknown_arguments(),
    89          ()
    90      )
    

    the terminal shows

    AssertionError: Tuples differ: ((), {}) != ()
    

    I change the expectation to match

    87        self.assertEqual(
    88            src.functions.w_unknown_arguments(),
    89            ((), {})
    90        )
    91
    92
    93# Exceptions seen
    

    the test passes

The function reads

This is why the update method of dictionaries can take a dictionary as input


close the project

  • I close the file(s) I have open in the editor(s)

  • I click in the terminal and exit the tests with ctrl+c on the keyboard

  • I deactivate the virtual environment

    deactivate
    

    the terminal goes back to the command line, (.venv) is no longer on the left side

    .../pumping_python/functions
    
  • I change directory to the parent of functions

    cd ..
    

    the terminal shows

    .../pumping_python
    

    I am back in the pumping_python directory


review

I ran tests to show that I can make functions with

as a reminder

How many questions can you answer after going through this chapter?


code from the chapter

Do you want to see all the CODE I typed in this chapter?


what is next?

you have covered a bit so far and know

Would you like to test how to pass values from tests to functions with assert methods?


please leave a review