what is a function?

A function is code that is callable, this means I can write code to do something one time, and use it to do the thing at a different time from when I write it, by calling the name.

Using a function can make code simpler, easier to read, test, reuse, maintain and improve - all the good things.

Part of Computer Programming is sending input data to a process and getting output data back, you can think of it like this

input_data -> process -> output_data

I think of it like mapping a function f in Mathematics with inputs x and output y

\[f(x) -> y\]

in other words

                f(x) -> y
function(input_data) -> output_data

the function does something (the process) with input_data and gives me output_data as the result.

functions are made with the def keyword in Python, a name, parentheses and a colon at the end. The code that makes up the function is indented to the right on the line after the colon.

def name_of_function():
    code indented to the right
    more code indented to the right
    ...

preview

These 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_why_use_a_function(self):
  8        def add_x(x=3, y=0):
  9            return x + y
 10
 11        self.assertEqual(add_x(y=0), 3)
 12        self.assertEqual(add_x(y=1), 4)
 13        self.assertEqual(add_x(y=2), 5)
 14        self.assertEqual(add_x(y=3), 6)
 15        self.assertEqual(add_x(y=4), 7)
 16        self.assertEqual(add_x(y=5), 8)
 17        self.assertEqual(add_x(y=6), 9)
 18        self.assertEqual(add_x(y=7), 10)
 19        self.assertEqual(add_x(y=8), 11)
 20        self.assertEqual(add_x(y=9), 12)
 21
 22    def test_making_a_function_w_pass(self):
 23        self.assertIsNone(src.functions.w_pass())
 24
 25    def test_making_a_function_w_return(self):
 26        self.assertIsNone(src.functions.w_return())
 27
 28    def test_making_a_function_w_return_none(self):
 29        self.assertIsNone(src.functions.w_return_none())
 30
 31    def test_what_happens_after_a_function_returns(self):
 32        self.assertIsNone(src.functions.return_is_last())
 33
 34    def test_constant_function(self):
 35        self.assertEqual(
 36            src.functions.constant(),
 37            'the same thing'
 38        )
 39
 40    def test_identity_function(self):
 41        self.assertIsNone(src.functions.identity(None))
 42        self.assertEqual(src.functions.identity(object), object)
 43
 44    def test_functions_w_positional_arguments(self):
 45        self.assertEqual(
 46            src.functions.w_positional_arguments('first', 'last'),
 47            ('first', 'last')
 48        )
 49        self.assertEqual(
 50            src.functions.w_positional_arguments('last', 'first'),
 51            ('last', 'first')
 52        )
 53
 54    def test_functions_w_keyword_arguments(self):
 55        self.assertEqual(
 56            src.functions.w_keyword_arguments(
 57                first_input='first', last_input='last',
 58            ),
 59            ('first', 'last')
 60        )
 61        self.assertEqual(
 62            src.functions.w_keyword_arguments(
 63                last_input='last', first_input='first',
 64            ),
 65            ('first', 'last')
 66        )
 67        self.assertEqual(
 68            src.functions.w_keyword_arguments('last', 'first'),
 69            ('last', 'first')
 70        )
 71
 72    def test_functions_w_positional_and_keyword_arguments(self):
 73        self.assertEqual(
 74            src.functions.w_positional_and_keyword_arguments(
 75                'first', last_input='last',
 76            ),
 77            ('first', 'last')
 78        )
 79
 80    def test_functions_w_default_arguments(self):
 81        self.assertEqual(
 82            src.functions.w_default_arguments('jane'),
 83            ('jane', 'doe')
 84        )
 85        self.assertEqual(
 86            src.functions.w_default_arguments('joe', 'blow'),
 87            ('joe', 'blow')
 88        )
 89
 90    def test_functions_w_unknown_arguments(self):
 91        self.assertEqual(
 92            src.functions.w_unknown_arguments(
 93                0, 1, 2, 3, a=4, b=5, c=6, d=7,
 94            ),
 95            ((0, 1, 2, 3), {'a': 4, 'b': 5, 'c': 6, 'd': 7})
 96        )
 97        self.assertEqual(
 98            src.functions.w_unknown_arguments(0, 1, 2, 3),
 99            ((0, 1, 2, 3), {})
100        )
101        self.assertEqual(
102            src.functions.w_unknown_arguments(a=4, b=5, c=6, d=7),
103            ((), dict(a=4, b=5, c=6, d=7))
104        )
105        self.assertEqual(
106            src.functions.w_unknown_arguments(),
107            ((), {})
108        )
109
110
111# Exceptions seen
112# AssertionError
113# NameError
114# AttributeError
115# TypeError
116# SyntaxError

questions about functions

Here are questions you can answer after going through this chapter


start the project

  • I name this project functions

  • I open a terminal

  • 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 in the functions folder

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

    mkdir src
    

    the terminal goes back to the command line

    .../pumping_python/functions
    
  • I make a Python file to hold the source code in the src directory

    touch src/functions.py
    

    Note

    on Windows without Windows Subsystem for Linux use New-Item src/functions.py not 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 make the tests directory a Python package

    Danger

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

    touch tests/__init__.py
    

    Note

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

    New-Item tests/__init__.py
    

    the terminal goes back to the command line

  • I make a Python file for the tests in the tests directory

    touch tests/test_functions.py
    

    Note

    on Windows without Windows Subsystem for Linux use New-Item tests/test_functions.py not 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 use the terminal to open a file in the Integrated Development Environment (IDE) by typing the name of the program and the name of the file. That means when I type this in the terminal

    code tests/test_functions.py
    

    Visual Studio Code opens test_functions.py 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 requirements file for the Python packages I need

    echo "pytest" > requirements.txt
    

    the terminal goes back to the command line

  • I add pytest-watcher to the file

    echo "pytest-watcher" >> requirements.txt
    

    the terminal goes back to the command line

  • I setup the project with uv

    uv init
    

    the terminal shows

    Initialized project `functions`
    

    then goes back to the command line

  • I remove main.py from the project

    rm main.py
    

    the terminal goes back to the command line

  • I install the Python packages listed in the requirements file

    uv add --requirement requirements.txt
    

    the terminal shows it installed the Python packages

  • I run the tests automatically

    uv run pytest-watcher . --now
    

    the terminal shows

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

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

     4class TestFunctions(unittest.TestCase):
     5
     6    def test_failure(self):
     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_why_use_a_function

Why use a function when I can just write code to do the thing I want? Let’s see


RED: make it fail


  • I change test_failure to test_why_use_a_function with an assertion

     4class TestFunctions(unittest.TestCase):
     5
     6    def test_why_use_a_function(self):
     7        self.assertEqual(1+0, 0)
     8
     9
    10# Exceptions seen
    

    the terminal shows AssertionError

    AssertionError: 1 != 0
    

GREEN: make it pass


I change the expectation to match reality

7        self.assertEqual(1+0, 1)

the test passes


REFACTOR: make it better


  • I add another assertion

    6    def test_why_use_a_function(self):
    7        self.assertEqual(1+0, 1)
    8        self.assertEqual(1+1, 1)
    

    the terminal shows AssertionError

    AssertionError: 2 != 1
    
  • I change the expectation

    8        self.assertEqual(1+1, 2)
    

    the test passes

  • I add another assertion

    6    def test_why_use_a_function(self):
    7        self.assertEqual(1+0, 1)
    8        self.assertEqual(1+1, 2)
    9        self.assertEqual(1+2, 2)
    

    the terminal shows AssertionError

    AssertionError: 3 != 2
    
  • I change the expectation

    9        self.assertEqual(1+2, 3)
    

    the test passes

  • I add an assertion

     6    def test_why_use_a_function(self):
     7        self.assertEqual(1+0, 1)
     8        self.assertEqual(1+1, 2)
     9        self.assertEqual(1+2, 3)
    10        self.assertEqual(1+3, 3)
    

    the terminal shows AssertionError

    AssertionError: 4 != 3
    
  • I change the expectation

    10        self.assertEqual(1+3, 4)
    

    the test passes

  • I add an assertion

     9        self.assertEqual(1+2, 3)
    10        self.assertEqual(1+3, 4)
    11        self.assertEqual(1+4, 4)
    12
    13
    14# Exceptions seen
    15# AssertionError
    

    the terminal shows AssertionError

    AssertionError: 5 != 4
    
  • I change the expectation

    11        self.assertEqual(1+4, 5)
    

    the test passes

  • I add an assertion

    10        self.assertEqual(1+3, 4)
    11        self.assertEqual(1+4, 5)
    12        self.assertEqual(1+5, 5)
    13
    14
    15# Exceptions seen
    

    the terminal shows AssertionError

    AssertionError: 6 != 5
    
  • I change the expectation

            self.assertEqual(1+5, 6)
    

    the test passes

  • I add another assertion

    11        self.assertEqual(1+4, 5)
    12        self.assertEqual(1+5, 6)
    13        self.assertEqual(1+6, 6)
    14
    15
    16# Exceptions seen
    

    the terminal shows AssertionError

    AssertionError: 7 != 6
    
  • I change the expectation

    13        self.assertEqual(1+6, 7)
    

    the test passes

  • I add an assertion

    12        self.assertEqual(1+5, 6)
    13        self.assertEqual(1+6, 7)
    14        self.assertEqual(1+7, 7)
    15
    16
    17# Exceptions seen
    

    the terminal shows AssertionError

    AssertionError: 8 != 7
    
  • I change the expectation

    14        self.assertEqual(1+7, 8)
    

    the test passes

  • I add another assertion

    13        self.assertEqual(1+6, 7)
    14        self.assertEqual(1+7, 8)
    15        self.assertEqual(1+8, 8)
    16
    17
    18# Exceptions seen
    

    the terminal shows AssertionError

    AssertionError: 9 != 8
    
  • I change the expectation

    15        self.assertEqual(1+8, 9)
    

    the test passes

  • I add an assertion

    15        self.assertEqual(1+7, 8)
    16        self.assertEqual(1+8, 9)
    17        self.assertEqual(1+9, 9)
    18
    19
    20# Exceptions seen
    

    the terminal shows AssertionError

    AssertionError: 10 != 9
    
  • I change the expectation

     6    def test_why_use_a_function(self):
     7        self.assertEqual(1+0, 1)
     8        self.assertEqual(1+1, 2)
     9        self.assertEqual(1+2, 3)
    10        self.assertEqual(1+3, 4)
    11        self.assertEqual(1+4, 5)
    12        self.assertEqual(1+5, 6)
    13        self.assertEqual(1+6, 7)
    14        self.assertEqual(1+7, 8)
    15        self.assertEqual(1+8, 9)
    16        self.assertEqual(1+9, 10)
    17
    18
    19# Exceptions seen
    

    the test passes

  • all those assertions test what happens when I add a number to 1, what if I want to test what happens when I add a number to 2? I would have to change 1 in 10 places. I change 1 to 2 in the calculation

     6    def test_why_use_a_function(self):
     7        self.assertEqual(2+0, 1)
     8        self.assertEqual(2+1, 2)
     9        self.assertEqual(2+2, 3)
    10        self.assertEqual(2+3, 4)
    11        self.assertEqual(2+4, 5)
    12        self.assertEqual(2+5, 6)
    13        self.assertEqual(2+6, 7)
    14        self.assertEqual(2+7, 8)
    15        self.assertEqual(2+8, 9)
    16        self.assertEqual(2+9, 10)
    

    the terminal shows AssertionError

    AssertionError: 2 != 1
    
  • I change the expectation of each assertion

     6    def test_why_use_a_function(self):
     7        self.assertEqual(2+0, 2)
     8        self.assertEqual(2+1, 3)
     9        self.assertEqual(2+2, 4)
    10        self.assertEqual(2+3, 5)
    11        self.assertEqual(2+4, 6)
    12        self.assertEqual(2+5, 7)
    13        self.assertEqual(2+6, 8)
    14        self.assertEqual(2+7, 9)
    15        self.assertEqual(2+8, 10)
    16        self.assertEqual(2+9, 11)
    

    the test passes

  • What if I want to test what happens when I add 3 to a number? Wait! No more, please, there has to be a better way. I can use a function for the parts that repeat, I add one to the test

     4class TestFunctions(unittest.TestCase):
     5
     6    def test_why_use_a_function(self):
     7        def add_x(x=2, y=0):
     8            return x + y
     9
    10        self.assertEqual(2+0, 2)
    
  • then I use it in the first assertion

     6    def test_why_use_a_function(self):
     7        def add_x(x=2, y=0):
     8            return x + y
     9
    10        # self.assertEqual(2+0, 2)
    11        self.assertEqual(add_x(y=0), 2)
    12        self.assertEqual(2+1, 3)
    

    the test is still green

  • I remove the comment and use the add_x function in the other assertions

     6    def test_why_use_a_function(self):
     7        def add_x(x=2, y=0):
     8            return x + y
     9
    10        self.assertEqual(add_x(y=0), 2)
    11        self.assertEqual(add_x(y=1), 3)
    12        self.assertEqual(add_x(y=2), 4)
    13        self.assertEqual(add_x(y=3), 5)
    14        self.assertEqual(add_x(y=4), 6)
    15        self.assertEqual(add_x(y=5), 7)
    16        self.assertEqual(add_x(y=6), 8)
    17        self.assertEqual(add_x(y=7), 9)
    18        self.assertEqual(add_x(y=8), 10)
    19        self.assertEqual(add_x(y=9), 11)
    

    still green

  • Now if I want to test what happens when I add 3 to a number, I only have to make that change in one place, then change the results since those will change as well

    6    def test_why_use_a_function(self):
    7        def add_x(x=3, y=0):
    8            return x + y
    

    the terminal shows AssertionError

    AssertionError: 3 != 2
    
  • I change the expectations for the assertions one at a time

     6    def test_why_use_a_function(self):
     7        def add_x(x=3, y=0):
     8            return x + y
     9
    10        self.assertEqual(add_x(y=0), 3)
    11        self.assertEqual(add_x(y=1), 4)
    12        self.assertEqual(add_x(y=2), 5)
    13        self.assertEqual(add_x(y=3), 6)
    14        self.assertEqual(add_x(y=4), 7)
    15        self.assertEqual(add_x(y=5), 8)
    16        self.assertEqual(add_x(y=6), 9)
    17        self.assertEqual(add_x(y=7), 10)
    18        self.assertEqual(add_x(y=8), 11)
    19        self.assertEqual(add_x(y=9), 12)
    20
    21
    22# Exceptions seen
    23# AssertionError
    

I can use a function to remove repetition. Is there a better way to handle the results changing?


test_making_a_function_w_pass

I can make a function with the pass keyword


RED: make it fail


I add a new test

 6    def test_why_use_a_function(self):
 7        def add_x(x=3, y=0):
 8            return x + y
 9
10        self.assertEqual(add_x(y=0), 3)
11        self.assertEqual(add_x(y=1), 4)
12        self.assertEqual(add_x(y=2), 5)
13        self.assertEqual(add_x(y=3), 6)
14        self.assertEqual(add_x(y=4), 7)
15        self.assertEqual(add_x(y=5), 8)
16        self.assertEqual(add_x(y=6), 9)
17        self.assertEqual(add_x(y=7), 10)
18        self.assertEqual(add_x(y=8), 11)
19        self.assertEqual(add_x(y=9), 12)
20
21    def test_making_a_function_w_pass(self):
22        self.assertIsNone(src.functions.w_pass())
23
24
25# Exceptions seen

the terminal shows NameError

NameError: name 'src' is not defined

GREEN: make it pass


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

    21    def test_making_a_function_w_pass(self):
    22        self.assertIsNone(src.functions.w_pass())
    23
    24
    25# Exceptions seen
    26# AssertionError
    27# NameError
    
  • 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 AttributeError to the list of Exceptions seen

    26# Exceptions seen
    27# AssertionError
    28# ModuleNotFoundError
    29# AttributeError
    
  • I open functions.py from the src folder in the editor

  • I add a function definition to functions.py

    1def w_pass():
    2    pass
    

    the test passes

    • the test checks if the result of the call to w_pass in functions.py in the src folder also known as src.functions.w_pass, is None

    • the function definition simply says pass and the test passes

    • pass is a special keyword that allows the function definition to follow Python language rules

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

I can make a function with pass


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

22    def test_making_a_function_w_pass(self):
23        self.assertIsNone(src.functions.w_pass())
24
25    def test_making_a_function_w_return(self):
26        self.assertIsNone(src.functions.w_return())
27
28
29# Exceptions seen

the terminal shows AttributeError

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

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


GREEN: make it pass


I add the new function with the pass keyword 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 and they both return None, because “all functions return None by default, as if they have an invisible line that says return None”, which leads me to the next test

I can make a function with a return statement


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

25    def test_making_a_function_w_return(self):
26        self.assertIsNone(src.functions.w_return())
27
28    def test_making_a_function_w_return_none(self):
29        self.assertIsNone(src.functions.w_return_none())
30
31
32# 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
    

    the test is still green

  • I change None to something

     9def w_return_none():
    10    return 'something'
    

    the terminal shows AssertionError

    AssertionError: 'something' is not None
    
  • I undo the change

     9def w_return_none():
    10    return None
    

    the test is green again

I have 3 functions with different statements and they all return None, because “all functions return None by default, as if they have an invisible line that says …” ah, the last function has a line that clearly says return None for everyone to see.

I like to write my functions this way, so that anyone can see what the function returns.

I can make a function with return None


test_what_happens_after_a_function_returns

The return statement is the last thing that runs in a function.


RED: make it fail


I add a test to test_functions.py

28    def test_making_a_function_w_return_none(self):
29        self.assertIsNone(src.functions.w_return_none())
30
31    def test_what_happens_after_a_function_returns(self):
32        self.assertIsNone(src.functions.return_is_last())
33
34
35# Exceptions seen

the terminal shows AttributeError

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

functions.py does not have a definition for it yet


GREEN: make it pass


I add a function to functions.py

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

the test passes


REFACTOR: make it better



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

31    def test_what_happens_after_a_function_returns(self):
32        self.assertIsNone(src.functions.return_is_last())
33
34    def test_constant_function(self):
35        self.assertEqual(
36            src.functions.constant(),
37            'the same thing'
38        )
39
40
41# 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

    13def return_is_last():
    14    return None
    15    return 'will not run'
    16
    17
    18def constant():
    19    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 the same

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

    the test passes

A constant function always returns 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?

a constant function always returns the same thing


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

34    def test_constant_function(self):
35        self.assertEqual(
36            src.functions.constant(),
37            'the same thing'
38        )
39
40    def test_identity_function(self):
41        self.assertIsNone(src.functions.identity(None))
42
43
44# Exceptions seen

the terminal shows AttributeError

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

because functions.py has no identity?


GREEN: make it pass


  • I add a function to functions.py

    18def constant():
    19    return 'the same thing'
    20
    21
    22def identity():
    23    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 TypeError to the list of Exceptions seen in test_functions.py

    44# Exceptions seen
    45# AssertionError
    46# NameError
    47# AttributeError
    48# TypeError
    
  • I add a name in parentheses for the identity function to take input in functions.py

    22def identity(the_input):
    23    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, this test passes 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

    40def test_identity_function(self):
    41    self.assertIsNone(src.functions.identity(None))
    42    self.assertEqual(src.functions.identity(object), object)
    

    the terminal shows AssertionError

    AssertionError: None != <class 'object'>
    

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

  • When I make the identity function in functions.py return what it gets

    22def identity(the_input):
    23    return the_input
    

    the test passes

I sometimes use the Identity Function when I am testing, to see if my test is connected to what I am testing. If I can send something and get it back, I can start making changes to see how it affects the output.

The Identity Function returns its input as output

So far, the functions take no input or one input, the following tests use functions that take more than one input.


test_functions_w_positional_arguments


RED: make it fail


I add a failing test to test_functions.py

40    def test_identity_function(self):
41        self.assertIsNone(src.functions.identity(None))
42        self.assertEqual(src.functions.identity(object), object)
43
44    def test_functions_w_positional_arguments(self):
45        self.assertEqual(
46            src.functions.w_positional_arguments('first', 'last'),
47            ('first', 'last')
48        )
49
50
51# Exceptions seen

the terminal shows AttributeError

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

because …


GREEN: make it pass


  • I add a function to functions.py

    22def identity(the_input):
    23    return the_input
    24
    25
    26def w_positional_arguments():
    27    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

    26def w_positional_arguments(first_input):
    27    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

    26def w_positional_arguments(first_input, last_input):
    27    return None
    

    the terminal shows AssertionError

    AssertionError: None != ('first', 'last')
    
  • I change the return statement to make the function return what it gets

    26def w_positional_arguments(first_input, last_input):
    27    return first_input, last_input
    

    the test passes


REFACTOR: make it better


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

    44    def test_functions_w_positional_arguments(self):
    45        self.assertEqual(
    46            src.functions.w_positional_arguments('first', 'last'),
    47            ('first', 'last')
    48        )
    49        self.assertEqual(
    50            src.functions.w_positional_arguments('last', 'first'),
    51            ('first', 'last')
    52        )
    53
    54
    55# Exceptions seen
    

    the terminal shows AssertionError

    AssertionError: Tuples differ: ('last', 'first') != ('first', 'last')
    
  • I change the expectation of the assertion

    44    def test_functions_w_positional_arguments(self):
    45        self.assertEqual(
    46            src.functions.w_positional_arguments('first', 'last'),
    47            ('first', 'last')
    48        )
    49        self.assertEqual(
    50            src.functions.w_positional_arguments('last', 'first'),
    51            ('last', 'first')
    52        )
    

    the test passes

    Note

    • w_positional_arguments in functions.py in the src folder will always

      return first_input, last_input
      
    • src.functions.w_positional_arguments('first', 'last') calls w_positional_arguments in functions.py in the src folder, with 'first' and 'last' as input, which is the same as

      return 'first', 'last'
      

      because first_input is 'first' and last_input is 'last' in the call to w_positional_arguments which will always

      return first_input, last_input
      
    • src.functions.w_positional_arguments('last', 'first') calls w_positional_arguments in functions.py in the src folder, with 'last' and 'first' as input, which is the same as

      return 'last', 'first'
      

      because first_input is 'last' and last_input is 'first' in the call to w_positional_arguments which will always

      return first_input, last_input
      

    I must give input in the order a function expects when I use positional arguments, because it uses input in the order it gets them

I can call functions with positional arguments


test_functions_w_keyword_arguments

There is a problem with using positional arguments, the inputs must always be given in the right order. This means the function does something different when it gets input out of order.

I can use Keyword Arguments to make sure it does what I want even when I send input out of order.


RED: make it fail


I add a new test to test_functions.py

44    def test_functions_w_positional_arguments(self):
45        self.assertEqual(
46            src.functions.w_positional_arguments('first', 'last'),
47            ('first', 'last')
48        )
49        self.assertEqual(
50            src.functions.w_positional_arguments('last', 'first'),
51            ('last', 'first')
52        )
53
54    def test_functions_w_keyword_arguments(self):
55        self.assertEqual(
56            src.functions.w_keyword_arguments(
57                first_input='first', last_input='last',
58            ),
59            ('first', 'last')
60        )
61
62
63# 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

    26def w_positional_arguments(first_input, last_input):
    27    return first_input, last_input
    28
    29
    30def w_keyword_arguments():
    31    return None
    

    the terminal shows TypeError

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

    30def w_keyword_arguments(first_input):
    31    return None
    

    the terminal shows TypeError

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

    30def w_keyword_arguments(first_input, last_input):
    31    return None
    

    the terminal shows AssertionError

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

    I change the return statement

    30def w_keyword_arguments(first_input, last_input):
    31    return first_input, last_input
    

    the test passes


REFACTOR: make it better


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

    54    def test_functions_w_keyword_arguments(self):
    55        self.assertEqual(
    56            src.functions.w_keyword_arguments(
    57                first_input='first', last_input='last',
    58            ),
    59            ('first', 'last')
    60        )
    61        self.assertEqual(
    62            src.functions.w_keyword_arguments(
    63                last_input='last', first_input='first',
    64            ),
    65            ('last', 'first')
    66        )
    

    the terminal shows AssertionError

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

    the order stayed the same

  • I change the expectation to match

    54    def test_functions_w_keyword_arguments(self):
    55        self.assertEqual(
    56            src.functions.w_keyword_arguments(
    57                first_input='first', last_input='last',
    58            ),
    59            ('first', 'last')
    60        )
    61        self.assertEqual(
    62            src.functions.w_keyword_arguments(
    63                last_input='last', first_input='first',
    64            ),
    65            ('first', 'last')
    66        )
    

    the test passes. I can give the input in any order when I use keyword arguments

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

    61        self.assertEqual(
    62            src.functions.w_keyword_arguments(
    63                last_input='last', first_input='first',
    64            ),
    65            ('first', 'last')
    66        )
    67        self.assertEqual(
    68            src.functions.w_keyword_arguments('last', 'first'),
    69            ('first', 'last')
    70        )
    71
    72
    73# Exceptions seen
    

    the terminal shows AssertionError

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

    the function uses the order (positions) when I do not use the names

  • I change the expectation to match

    54    def test_functions_w_keyword_arguments(self):
    55        self.assertEqual(
    56            src.functions.w_keyword_arguments(
    57                first_input='first', last_input='last',
    58            ),
    59            ('first', 'last')
    60        )
    61        self.assertEqual(
    62            src.functions.w_keyword_arguments(
    63                last_input='last', first_input='first',
    64            ),
    65            ('first', 'last')
    66        )
    67        self.assertEqual(
    68            src.functions.w_keyword_arguments('last', 'first'),
    69            ('last', 'first')
    70        )
    71
    72
    73# Exceptions seen
    

    the test passes

Note

w_keyword_arguments and w_positional_arguments are the same functions, they always

return first_input, last_input

Their names are different

def w_positional_arguments(first_input, last_input):
def w_keyword_arguments(first_input, last_input):

The difference that matters in the tests is how I call the functions

  • I have to give the input in order when I use positional arguments

    w_positional_arguments('first', 'last') == return ('first', 'last')
    w_positional_arguments('last', 'first') == return ('last', 'first')
       w_keyword_arguments('last', 'first') == return ('last', 'first')
    
  • I can give the input in any order when I use keyword arguments because I give values for the names in parentheses from the function definition when I call it

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

    both of these statements are the same as

    return 'first', 'last'
    

    because first_input is 'first' and last_input is 'last' in the call to w_keyword_arguments which will always

    return first_input, last_input
    

I can call a function with keyword arguments


test_functions_w_positional_and_keyword_arguments

I can write functions that take both positional and keyword arguments


RED: make it fail


I add a failing test to test_functions.py

54      def test_functions_w_keyword_arguments(self):
55          self.assertEqual(
56              src.functions.w_keyword_arguments(
57                  first_input='first', last_input='last',
58              ),
59              ('first', 'last')
60          )
61          self.assertEqual(
62              src.functions.w_keyword_arguments(
63                  last_input='last', first_input='first',
64              ),
65              ('first', 'last')
66          )
67          self.assertEqual(
68              src.functions.w_keyword_arguments('last', 'first'),
69              ('last', 'first')
70          )
71
72      def test_functions_w_positional_and_keyword_arguments(self):
73          self.assertEqual(
74              src.functions.w_positional_and_keyword_arguments(
75                  last_input='last', 'first',
76              ),
77              ('first', 'last')
78          )
79
80
81  # Exceptions seen

the terminal shows SyntaxError

SyntaxError: positional argument follows keyword argument

I cannot put keyword arguments before positional arguments


GREEN: make it pass


  • 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 arguments to follow Python rules

    72    def test_functions_w_positional_and_keyword_arguments(self):
    73        self.assertEqual(
    74            src.functions.w_positional_and_keyword_arguments(
    75                'first', last_input='last',
    76            ),
    77            ('first', 'last')
    78        )
    

    the terminal shows AttributeError

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

    30  def w_keyword_arguments(first_input, last_input):
    31      return first_input, last_input
    32
    33
    34  def w_positional_and_keyword_arguments():
    35      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

    34def w_positional_and_keyword_arguments(last_input):
    35    return None
    

    the terminal shows

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

    34def w_positional_and_keyword_arguments(last_input, first_input):
    35    return None
    

    the terminal shows TypeError

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

    I cannot put positional arguments after keyword arguments. Python cannot tell the difference between the 2 values because last_input is both the second positional argument and passed in as a keyword argument

  • I change the order of the names in parentheses

    34def w_positional_and_keyword_arguments(first_input, last_input):
    35    return None
    

    the terminal shows AssertionError

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

    34def w_positional_and_keyword_arguments(first_input, last_input):
    35    return first_input, last_input
    

    the test passes.

I can call a function with positional and keyword arguments


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


RED: make it fail


I add a failing test to test_functions.py

72    def test_functions_w_positional_and_keyword_arguments(self):
73        self.assertEqual(
74            src.functions.w_positional_and_keyword_arguments(
75                'first', last_input='last',
76            ),
77            ('first', 'last')
78        )
79
80    def test_functions_w_default_arguments(self):
81        self.assertEqual(
82            src.functions.w_default_arguments('jane', last_name='doe'),
83            ('jane', 'doe')
84        )
85
86
87# 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

34def w_positional_and_keyword_arguments(first_input, last_input):
35    return first_input, last_input
36
37
38def w_default_arguments(first_name, last_name):
39    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

    80    def test_functions_w_default_arguments(self):
    81        self.assertEqual(
    82            src.functions.w_default_arguments('jane'),
    83            ('jane', 'doe')
    84        )
    

    the terminal shows TypeError

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

    the last_name argument MUST be given when this 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 because the last_name argument no longer has to be given when this function is called

    Note

    If I call the function without the last_name argument

    w_default_arguments('jane')
    

    it is the same as when I call it with the default value

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

    which is the same as

    return 'jane', 'doe'
    

    because w_default_arguments will always

    return first_name, last_name
    
  • I add another assertion to test_functions.py to show that I can still call the function with different values

    80    def test_functions_w_default_arguments(self):
    81        self.assertEqual(
    82            src.functions.w_default_arguments('jane'),
    83            ('jane', 'doe')
    84        )
    85        self.assertEqual(
    86            src.functions.w_default_arguments('joe', 'blow'),
    87            ()
    88        )
    89
    90
    91# Exceptions seen
    

    the terminal shows AssertionError

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

    I change the expectation to match

    80    def test_functions_w_default_arguments(self):
    81        self.assertEqual(
    82            src.functions.w_default_arguments('jane'),
    83            ('jane', 'doe')
    84        )
    85        self.assertEqual(
    86            src.functions.w_default_arguments('joe', 'blow'),
    87            ('joe', 'blow')
    88        )
    89
    90
    91# Exceptions seen
    

    the test passes

Note

w_keyword_arguments, w_positional_arguments, w_positional_and_keyword_arguments and w_default_arguments are the same functions, they always

return first_input, last_input

their names are different, w_default_arguments uses different names for the input and has a default value, it will always

return first_name, last_name

but first_input, first_name, last_input and last_name are just names, they could be any name

def w_positional_arguments(first_input, last_input):
def w_keyword_arguments(first_input, last_input):
def w_positional_and_keyword_arguments(first_input, last_input):
def w_default_arguments(first_name, last_name='doe'):

The difference that matters in the tests is how I call the functions

                       w_positional_arguments('first', 'last') == return 'first', 'last'
                       w_positional_arguments('last', 'first') == return 'last',  'first'
   w_keyword_arguments(first_input='first', last_input='last') == return 'first', 'last'
   w_keyword_arguments(last_input='last', first_input='first') == return 'first', 'last'
                          w_keyword_arguments('last', 'first') == return 'last', 'first'
w_positional_and_keyword_arguments('first', last_input='last') == return 'first', 'last'
                  w_default_arguments('jane', last_name='doe') == return 'jane', 'doe'
                                   w_default_arguments('jane') == return 'jane', 'doe'
                            w_default_arguments('joe', 'blow') == return 'joe', 'blow'

Tip

as a rule of thumb I use keyword arguments when I have 2 or more inputs so I do not have to remember the order


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

80    def test_functions_w_default_arguments(self):
81        self.assertEqual(
82            src.functions.w_default_arguments('jane'),
83            ('jane', 'doe')
84        )
85        self.assertEqual(
86            src.functions.w_default_arguments('joe', 'blow'),
87            ('joe', 'blow')
88        )
89
90    def test_functions_w_unknown_arguments(self):
91        self.assertEqual(
92            src.functions.w_unknown_arguments(
93                0, 1, 2, 3, a=4, b=5, c=6, d=7,
94            ),
95            None
96        )
97
98
99# 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

    38def w_default_arguments(first_name, last_name='doe'):
    39    return first_name, last_name
    40
    41
    42def w_unknown_arguments():
    43    return None
    

    the terminal shows TypeError

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

    42def w_unknown_arguments(a):
    43    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 if a is a positional or keyword argument in this case

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

    42def w_unknown_arguments(**kwargs):
    43    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

    42def w_unknown_arguments(**kwargs, x):
    43    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 change the order of the inputs in w_unknown_arguments in functions.py

    42def w_unknown_arguments(x, **kwargs):
    43    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 use what Python has to handle any number of positional arguments

    42def w_unknown_arguments(*args, **kwargs):
    43    return None
    

    the test passes


REFACTOR: make it better


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

    42def w_unknown_arguments(*positional_arguments, **keyword_arguments):
    43    return None
    

    the test is still green

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

    42def w_unknown_arguments(*positional_arguments, **keyword_arguments):
    43    return positional_arguments, keyword_arguments
    

    the terminal shows

    AssertionError: ((0, 1, 2, 3), {'a': 4, 'b': 5, 'c': 6, 'd': 7}) != None
    

    I get a tuple that has another tuple and a dictionary

  • I copy the tuple from the terminal and use it to change the expectation in test_functions_w_unknown_arguments in test_functions.py

    90    def test_functions_w_unknown_arguments(self):
    91        self.assertEqual(
    92            src.functions.w_unknown_arguments(
    93                0, 1, 2, 3, a=4, b=5, c=6, d=7,
    94            ),
    95            ((0, 1, 2, 3), {'a': 4, 'b': 5, 'c': 6, 'd': 7})
    96        )
    

    the test passes


how Python reads positional arguments

I want to see what happens when I call w_unknown_arguments with ONLY positional arguments. I add an assertion

 90    def test_functions_w_unknown_arguments(self):
 91        self.assertEqual(
 92            src.functions.w_unknown_arguments(
 93                0, 1, 2, 3, a=4, b=5, c=6, d=7,
 94            ),
 95            ((0, 1, 2, 3, ), {'a': 4, 'b': 5, 'c': 6, 'd': 7})
 96        )
 97        self.assertEqual(
 98            src.functions.w_unknown_arguments(0, 1, 2, 3),
 99            ()
100        )
101
102
103# Exceptions seen

the terminal shows AssertionError

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

I change the expectation to match

 97        self.assertEqual(
 98            src.functions.w_unknown_arguments(0, 1, 2, 3),
 99            ((0, 1, 2, 3), {})
100        )
101
102
103# 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

 97        self.assertEqual(
 98            src.functions.w_unknown_arguments(0, 1, 2, 3),
 99            ((0, 1, 2, 3))
100        )
101        self.assertEqual(
102            src.functions.w_unknown_arguments(a=4, b=5, c=6, d=7),
103            ()
104        )
105
106
107# Exceptions seen

the terminal shows

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

I change the expectation to match

101        self.assertEqual(
102            src.functions.w_unknown_arguments(a=4, b=5, c=6, d=7),
103            ((), dict(a=4, b=5, c=6, d=7))
104        )
105
106
107# Exceptions seen

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


how Python reads positional and keyword arguments

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

101        self.assertEqual(
102            src.functions.w_unknown_arguments(a=4, b=5, c=6, d=7),
103            ((), dict(a=4, b=5, c=6, d=7))
104        )
105        self.assertEqual(
106            src.functions.w_unknown_arguments(),
107            ()
108        )
109
110
111# Exceptions seen

the terminal shows

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

I change the expectation to match

 90    def test_functions_w_unknown_arguments(self):
 91        self.assertEqual(
 92            src.functions.w_unknown_arguments(
 93                0, 1, 2, 3, a=4, b=5, c=6, d=7,
 94            ),
 95            ((0, 1, 2, 3, ), {'a': 4, 'b': 5, 'c': 6, 'd': 7})
 96        )
 97        self.assertEqual(
 98            src.functions.w_unknown_arguments(0, 1, 2, 3),
 99            ((0, 1, 2, 3), {})
100        )
101        self.assertEqual(
102            src.functions.w_unknown_arguments(a=4, b=5, c=6, d=7),
103            ((), dict(a=4, b=5, c=6, d=7))
104        )
105        self.assertEqual(
106            src.functions.w_unknown_arguments(),
107            ((), {})
108        )
109
110
111# Exceptions seen

the test passes

Note

these statements are the same

w_unknown_arguments(0, 1, 2, 3, a=4, b=5, c=6, d=7)
w_unknown_arguments(*(0, 1, 2, 3), **dict(a=4, b=5, c=6, d=7))
w_unknown_arguments(*(0, 1, 2, 3), **{'a': 4, 'b': 5, 'c': 6, 'd': 7})
((0, 1, 2, 3, ), {'a': 4, 'b': 5, 'c': 6, 'd': 7})

because w_unknown_arguments in functions.py in the src folder will always

return positional_arguments, keyword_arguments

in this case

0, 1, 2, 3
*(0, 1, 2, 3)

are positional arguments which are taken as a tuple and

a=4, b=5, c=6, d=7
**dict(a=4, b=5, c=6, d=7)
**{'a': 4, 'b': 5, 'c': 6, 'd': 7}

are keyword arguments which are taken as a dictionary. The function reads positional arguments as tuples, and keyword arguments as dictionaries, which is why the update method of dictionaries can take a dictionary as input


close the project

  • I close test_functions.py and functions.py in the editor

  • I click in the terminal and use q on the keyboard to leave the tests and the terminal goes back to the command line

  • 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 about functions?


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 know what causes AttributeError?


rate pumping python

If this has been a 7 star experience for you, please CLICK HERE to leave a 5 star review of pumping python. It helps other people get into the book too