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
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
functionsI open a terminal
I make a directory for the project
mkdir functionsthe terminal goes back to the command line
.../pumping_pythonI change directory to the project
cd functionsthe terminal shows I am in the
functionsfolder.../pumping_python/functionsI make a directory for the source code
mkdir srcthe terminal goes back to the command line
.../pumping_python/functionsI make a Python file to hold the source code in the
srcdirectorytouch src/functions.pyNote
on Windows without Windows Subsystem for Linux use
New-Item src/functions.pynottouch src/functions.pyNew-Item src/functions.pythe terminal goes back to the command line
.../pumping_python/functionsI make a directory for the tests
mkdir teststhe terminal goes back to the command line
I make the
testsdirectory a Python packageDanger
use 2 underscores (__) before and after
initfor__init__.pynot_init_.pytouch tests/__init__.pyNote
on Windows without Windows Subsystem for Linux use
New-Item tests/__init__.pynottouch tests/__init__.pyNew-Item tests/__init__.pythe terminal goes back to the command line
I make a Python file for the tests in the
testsdirectorytouch tests/test_functions.pyNote
on Windows without Windows Subsystem for Linux use
New-Item tests/test_functions.pynottouch tests/test_functions.pyNew-Item tests/test_functions.pythe terminal goes back to the command line
I open
test_functions.pyin 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.pyVisual Studio Code opens
test_functions.pyin the editorI add the first failing test to
test_functions.py1import 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.txtthe terminal goes back to the command line
I add pytest-watcher to the file
echo "pytest-watcher" >> requirements.txtthe terminal goes back to the command line
I setup the project with uv
uv initthe terminal shows
Initialized project `functions`then goes back to the command line
I remove
main.pyfrom the projectrm main.pythe terminal goes back to the command line
I install the Python packages listed in the requirements file
uv add --requirement requirements.txtthe terminal shows it installed the Python packages
I run the tests automatically
uv run pytest-watcher . --nowthe 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:7to put the cursor on line 7 in the editorI add AssertionError to the list of Exceptions seen in
test_functions.py4class TestFunctions(unittest.TestCase): 5 6 def test_failure(self): 7 self.assertFalse(True) 8 9 10# Exceptions seen 11# AssertionErrorthen 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_failuretotest_why_use_a_functionwith an assertion4class TestFunctions(unittest.TestCase): 5 6 def test_why_use_a_function(self): 7 self.assertEqual(1+0, 0) 8 9 10# Exceptions seenthe 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 != 1I 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 != 2I 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 != 3I 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# AssertionErrorthe terminal shows AssertionError
AssertionError: 5 != 4I 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 seenthe terminal shows AssertionError
AssertionError: 6 != 5I 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 seenthe terminal shows AssertionError
AssertionError: 7 != 6I 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 seenthe terminal shows AssertionError
AssertionError: 8 != 7I 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 seenthe terminal shows AssertionError
AssertionError: 9 != 8I 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 seenthe terminal shows AssertionError
AssertionError: 10 != 9I 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 seenthe 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 to2? I would have to change1in 10 places. I change1to2in the calculation6 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 != 1I 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
3to 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 test4class 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_xfunction in the other assertions6 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
3to a number, I only have to make that change in one place, then change the results since those will change as well6 def test_why_use_a_function(self): 7 def add_x(x=3, y=0): 8 return x + ythe terminal shows AssertionError
AssertionError: 3 != 2I 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
NameError: name 'src' is not defined
GREEN: make it pass
I add NameError to the list of Exceptions seen in
test_functions.py21 def test_making_a_function_w_pass(self): 22 self.assertIsNone(src.functions.w_pass()) 23 24 25# Exceptions seen 26# AssertionError 27# NameErrorI 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.pyin thesrcfolder does not have anything namedw_passinside itI add AttributeError to the list of Exceptions seen
26# Exceptions seen 27# AssertionError 28# ModuleNotFoundError 29# AttributeErrorI open
functions.pyfrom thesrcfolder in the editorI add a function definition to
functions.py1def w_pass(): 2 passthe test passes
the test checks if the result of the call to
w_passinfunctions.pyin thesrcfolder also known assrc.functions.w_pass, is Nonethe 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
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
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 Nonethe test is still green
I change None to
something9def w_return_none(): 10 return 'something'the terminal shows AssertionError
AssertionError: 'something' is not NoneI undo the change
9def w_return_none(): 10 return Nonethe 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.
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
I change the return statement
13def return_is_last(): 14 return 'something'the terminal shows AssertionError
AssertionError: 'something' is not NoneI add another return statement
13def return_is_last(): 14 return 'something' 15 return Nonethe terminal still shows the same AssertionError because the return statement is the last thing to run in a function, the second return statement will not run
Note
The Integrated Development Environment (IDE) shows that the second return statement will not run by graying it out
I make
return None, the first return statement13def return_is_last(): 14 return None 15 return 'something'the test is green again
I change the second return statement as a reminder
13def return_is_last(): 14 return None 15 return 'will not run'the second return statement is now like a comment, and the test is still green because the return statement is the last thing to run in a function
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.py13def return_is_last(): 14 return None 15 return 'will not run' 16 17 18def constant(): 19 return Nonethe terminal shows AssertionError
AssertionError: None != 'the same thing'what the
constantfunction returns and what the test expects are differentI 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?
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.py18def constant(): 19 return 'the same thing' 20 21 22def identity(): 23 return NoneTypeError: identity() takes 0 positional arguments but 1 was giventhe definition for
identitydoes not allow inputs and the test sends None as inputI add TypeError to the list of Exceptions seen in
test_functions.py44# Exceptions seen 45# AssertionError 46# NameError 47# AttributeError 48# TypeErrorI add a name in parentheses for the
identityfunction to take input infunctions.py22def identity(the_input): 23 return Nonethe 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_functionintest_functions.py40def 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, yetWhen I make the
identityfunction infunctions.pyreturn what it gets22def identity(the_input): 23 return the_inputthe 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.py22def identity(the_input): 23 return the_input 24 25 26def w_positional_arguments(): 27 return NoneTypeError: w_positional_arguments() takes 0 positional arguments but 2 were givenI make the function take input by adding a name in parentheses
26def w_positional_arguments(first_input): 27 return NoneTypeError: w_positional_arguments() takes 1 positional argument but 2 were givenI make
w_positional_argumentstake another input by adding another name in parentheses26def w_positional_arguments(first_input, last_input): 27 return Nonethe 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_inputthe 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.pyto show this44 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 seenthe 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_argumentsinfunctions.pyin thesrcfolder will alwaysreturn first_input, last_inputsrc.functions.w_positional_arguments('first', 'last')callsw_positional_argumentsinfunctions.pyin thesrcfolder, with'first'and'last'as input, which is the same asreturn 'first', 'last'because
first_inputis'first'andlast_inputis'last'in the call tow_positional_argumentswhich will alwaysreturn first_input, last_inputsrc.functions.w_positional_arguments('last', 'first')callsw_positional_argumentsinfunctions.pyin thesrcfolder, with'last'and'first'as input, which is the same asreturn 'last', 'first'because
first_inputis'last'andlast_inputis'first'in the call tow_positional_argumentswhich will alwaysreturn 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
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.py26def w_positional_arguments(first_input, last_input): 27 return first_input, last_input 28 29 30def w_keyword_arguments(): 31 return NoneTypeError: 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 NoneTypeError: w_keyword_arguments() got an unexpected keyword argument 'last_input'. Did you meanI add the name for the other unexpected argument in parentheses
30def w_keyword_arguments(first_input, last_input): 31 return Nonethe 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_inputthe test passes
REFACTOR: make it better
I add another test with the keyword arguments given out of order in
test_functions.py54 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 seenthe 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 seenthe 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_inputis'first'andlast_inputis'last'in the call tow_keyword_argumentswhich will alwaysreturn first_input, last_input
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.py81# Exceptions seen 82# AssertionError 83# NameError 84# AttributeError 85# TypeError 86# SyntaxErrorI 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.py30 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 NoneTypeError: w_positional_and_keyword_arguments() got an unexpected keyword argument 'last_input'I add the name to the function definition in parentheses in
functions.py34def w_positional_and_keyword_arguments(last_input): 35 return Nonethe 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 NoneTypeError: 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_inputis both the second positional argument and passed in as a keyword argumentI change the order of the names in parentheses
34def w_positional_and_keyword_arguments(first_input, last_input): 35 return Nonethe 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_inputthe test passes.
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 tow_default_argumentsintest_functions.py80 def test_functions_w_default_arguments(self): 81 self.assertEqual( 82 src.functions.w_default_arguments('jane'), 83 ('jane', 'doe') 84 )TypeError: w_default_arguments() missing 1 required positional argument: 'last_name'the
last_nameargument MUST be given when this function is calledI make the argument a choice by giving it a default value in
functions.py33def w_default_arguments(first_name, last_name='doe'): 34 return first_name, last_namethe test passes because the
last_nameargument no longer has to be given when this function is calledNote
If I call the function without the
last_nameargumentw_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_argumentswill alwaysreturn first_name, last_nameI add another assertion to
test_functions.pyto show that I can still call the function with different values80 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 seenthe 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 seenthe 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.py38def w_default_arguments(first_name, last_name='doe'): 39 return first_name, last_name 40 41 42def w_unknown_arguments(): 43 return NoneTypeError: w_unknown_arguments() got an unexpected keyword argument 'a'I add the name to the function definition
42def w_unknown_arguments(a): 43 return NoneTypeError: 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
ais a positional or keyword argument in this casePython has a way for a function to get any number of keyword arguments without knowing how many they are. I use it to replace
ain the parentheses42def w_unknown_arguments(**kwargs): 43 return NoneTypeError: w_unknown_arguments() takes 0 positional arguments but 4 were givenI add a name for the first positional argument
42def w_unknown_arguments(**kwargs, x): 43 return Nonethe terminal shows SyntaxError
SyntaxError: arguments cannot follow var-keyword argumenta reminder that I cannot put positional arguments after keyword arguments
I change the order of the inputs in
w_unknown_argumentsinfunctions.py42def w_unknown_arguments(x, **kwargs): 43 return NoneTypeError: w_unknown_arguments() takes 1 positional argument but 4 were givenI 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 Nonethe test passes
REFACTOR: make it better
*args, **kwargsis Python convention. I change the names to be clearer42def w_unknown_arguments(*positional_arguments, **keyword_arguments): 43 return Nonethe 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_argumentsthe terminal shows
AssertionError: ((0, 1, 2, 3), {'a': 4, 'b': 5, 'c': 6, 'd': 7}) != NoneI 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_argumentsintest_functions.py90 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.pyandfunctions.pyin the editorI 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
functionscd ..the terminal shows
.../pumping_pythonI am back in the
pumping_pythondirectory
review
I ran tests to show that I can make functions with
the def keyword
as a reminder
code from the chapter
what is next?
you have covered a bit so far and know
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