functions

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

Programming involves providing a process with input data and the process returning output data

argument -> program -> output_data

I think of it mathematically as mapping a function f with inputs x and an output of y

\[f(x) -> y\]

in other words

program(argument) -> output_data

program is the function that processes argument to return output_data

functions are defined using the def keyword, a name, parentheses and a colon at the end

requirements

  • I open a terminal to run makePythonTdd.sh with functions as the name of the project

    ./makePythonTdd.sh functions
    

    on Windows without Windows Subsystem Linux use makePythonTdd.ps1

    ./makePythonTdd.ps1 functions
    

    it makes the folders and files that are needed, installs packages, runs the first test, and the terminal shows AssertionError

    E       AssertionError: True is not false
    
    tests/test_functions.py:7: AssertionError
    
  • I hold ctrl (windows/linux) or option (mac) on the keyboard and use the mouse to click on tests/test_functions.py:7 to open it in the editor

  • then change True to False

test_functions_w_pass

red: make it fail

  • I change test_failure to test_functions_w_pass

    import unittest
    
    
    class TestFunctions(unittest.TestCase):
    
        def test_functions_w_pass(self):
            self.assertIsNone(src.functions.function_w_pass())
    

    the terminal shows NameError

    
    

    I add it to the list of Exceptions encountered

    # Exceptions Encountered
    # AssertionError
    # NameError
    

green: make it pass

  • I make a file called functions.py in the project folder and the terminal shows AttributeError , which I add to the list of Exceptions encountered

    # Exceptions Encountered
    # AssertionError
    # ModuleNotFoundError
    # AttributeError
    
  • I add a function definition to functions.py

    def function_w_pass():
        pass
    

    and we have a passing test

    • the test checks if the value of the call to functions.function_w_pass is None

    • the function definition simply says pass yet the test passes

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

    • the test passes because in Python all functions return None by default, like the function has an invisible line that says return None


test_functions_w_return

red: make it fail

I add a new failing test to TestFunctions in test_functions.py to check that functions always return None

def test_functions_w_return(self):
    self.assertIsNone(functions.function_w_return())

the terminal shows AttributeError

green: make it pass

I add a new function to functions.py to make the test pass, this time with a return statement instead of pass

def function_w_return(self):
    return

the terminal shows this test also passes

I defined 2 functions with different statements in their body but they both return the same result, because “in Python all functions return None by default, like the function has an invisible line that says return None

test_functions_w_return_none

red: make it fail

I add one more test to the TestFunctions class in test_functions.py to help drive home the point

def test_functions_w_return_none(self):
    self.assertIsNone(
        functions.function_w_return_none()
    )

the terminal shows AttributeError

green: make it pass

from the Zen of Python: Explicit is better than implicit. I add a function definition to functions.py this time with an explicit return statement showing the value returned

def function_w_return_none():
    return None

and the terminal shows passing tests.


test_constant_functions

constant functions always return the same thing when called

red: make it fail

I add a test to test_functions.py

def test_constant_functions(self):
    self.assertEqual(functions.constant(), 'first_name')

the terminal shows AttributeError

green: make it pass

I change the function to make it pass

def constant():
    return 'first_name'

test_constant_functions_w_inputs

red: make it fail

I add a new test for a constant function that takes input

def test_constant_functions_w_inputs(self):
    self.assertEqual(
        src.functions.constant_w_inputs('Bob', 'James', 'Frank'),
        src.functions.constant()
    )
    self.assertEqual(
        functions.constant_w_inputs('a', 2, 'c', 3),
        src.functions.constant()
    )

the terminal shows AttributeError

green: make it pass

and I add a definition for it

def constant_w_inputs(*arguments):
    return constant()

the terminal shows passing tests


test_identity_functions

identity functions return their input as output

red: make it fail

I add a failing test to the TestFunctions class in test_functions.py

def test_identity_functions(self):
    self.assertEqual(functions.identity(False), False)

the terminal shows AttributeError

green: make it pass

  • I add a function definition to functions.py

    def identity():
        return None
    

    the terminal shows TypeError

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

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

  • I add the error to the list of Exceptions encountered

    # Exceptions Encountered
    # AssertionError
    # ModuleNotFoundError
    # AttributeError
    # TypeError
    
  • then I make identity in functions.py to take 1 positional argument

    def identity(argument):
        return None
    

    and the terminal shows AssertionError

    AssertionError: None != False
    

    because the result of calling functions.identity with False as input is None which is not equal to the expected result (False)

  • I change the definition of identity to make the test pass

    def identity(argument):
        return False
    

    the terminal shows passing tests. I am genius!

refactor: make it better

Wait a minute! Something is not quite right here. The definition for a identity function was that it returned the same thing it was given, the test passes when False is given as input, will it still pass when another value is given or will it always return False? Time to write a test

  • I add a new assertion to test_identity_functions

    def test_identity_functions(self):
        self.assertEqual(functions.identity(False), False)
        self.assertEqual(functions.identity(True), True)
    

    the terminal shows AssertionError

    AssertionError: False != True
    

    the function returns False instead of True in the second case, I am not all the way genius, yet

  • I change the definition of identity in functions.py

    def identity(argument):
        return argument
    

    the terminal shows passing tests. I have more confidence that the identity function will return its input.

  • I add more tests for good measure using the other Python data structures

    def test_identity_functions(self):
        self.assertEqual(functions.identity(False), False)
        self.assertEqual(functions.identity(True), True)
        self.assertEqual(functions.identity(None), False)
        self.assertEqual(functions.identity(int), False)
        self.assertEqual(functions.identity(str), False)
        self.assertEqual(functions.identity(tuple), False)
        self.assertEqual(functions.identity(list), False)
        self.assertEqual(functions.identity(set), False)
        self.assertEqual(functions.identity(dict), False)
    

    the terminal shows AssertionError for each line until I make the input match the output, proving that the identity function I have defined returns the input it is given. Hooray! I am genius again


test_functions_w_positional_arguments

I can make a function take more than one input

def test_functions_w_positional_arguments(self):
    self.assertEqual(
        functions.function_w_positional_arguments(
            'first_name', 'last_name'
        ),
        ('first_name', 'last_name')
    )

the terminal shows AttributeError

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

green: make it pass

  • I add a function to functions.py

    def function_w_positional_arguments(argument):
        return argument
    

    the terminal shows TypeError

    TypeError: function_w_positional_arguments() takes 1 positional argument but 2 were given
    

    I make the function take more than one argument

    def function_w_positional_arguments(
        argument, second
    ):
        return argument
    

    the terminal shows AssertionError

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

    I make it return the 2 arguments it receives

    def function_w_positional_arguments(
        argument, second
    ):
        return argument, second
    

    the test passes

refactor: make it better

How can I make this better?

  • I change the name of the first argument to be more descriptive

    def function_w_positional_arguments(
            first, second
        ):
        return first, second
    

    I still have passing tests

  • I add another assertion to make sure that function_w_positional_arguments outputs data in the order given

    def test_functions_w_positional_arguments(self):
        self.assertEqual(
            functions.function_w_positional_arguments(
                'first_name', 'last_name'
            ),
            ('first_name', 'last_name')
        )
        self.assertEqual(
            functions.function_w_positional_arguments(
                'last_name', 'first_name'
            ),
            ('first_name', 'last_name')
        )
    

    the terminal shows AssertionError

    AssertionError: Tuples differ: ('last_name', 'first_name') != ('first_name', 'last_name')
    
  • I change the test so it has the right output

    def test_functions_w_positional_arguments(self):
        self.assertEqual(
            functions.function_w_positional_arguments(
                'first_name', 'last_name'
            ),
            ('first_name', 'last_name')
        )
        self.assertEqual(
            functions.function_w_positional_arguments(
                'last_name', 'first_name'
            ),
            ('last_name', 'first_name')
        )
    

    the terminal shows passing

  • the function currently takes in 2 positional arguments.

test_functions_w_unknown_positional_arguments

  • There are scenarios where a function needs to take in more arguments, like when I do not know the number of positional arguments that will be passed to the function, I add a test for the case where the number of positional arguments received is not known

    def test_functions_w_unknown_positional_arguments(self):
        self.assertEqual(
            src.functions.function_w_unknown_positional_arguments(
                0, 1, 2, 3
            ),
            (0, 1, 2, 3)
        )
    

    the terminal shows AttributeError

    AttributeError: module 'src.functions' has no attribute 'function_w_unknown_positional_arguments'. Did you mean: 'function_w_positional_arguments'?
    

    I add the function with a starred expression like I did in test_constant_functions, to allow it take in any number of arguments

    def function_w_unknown_positional_arguments(*arguments):
        return arguments
    

    the test passes

  • I add another assertion to show that this function never needs to know the number of inputs it is receiving

    self.assertEqual(
        src.functions.function_w_unknown_positional_arguments(
            0, 1, 2, 3
        ),
        (0, 1, 2, 3)
    )
    self.assertEqual(
        src.functions.function_w_unknown_positional_arguments(
            None, bool, int, float, str, tuple, list, set, dict
        ),
        None
    )
    

    the terminal shows AssertionError

    AssertionError: (None, <class 'bool'>, <class 'int'>, <cl[87 chars]ct'>) != None
    

    I change the expectation to match

    self.assertEqual(
        src.functions.function_w_unknown_positional_arguments(
            None, bool, int, float, str, tuple, list, set, dict
        ),
        (None, bool, int, float, str, tuple, list, set, dict)
    )
    

    the test is green again


test_functions_w_keyword_arguments

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

To make sure the function behaves how we want regardless of what order the user gives the input I can use Keyword Arguments

red: make it fail

I add a new test to test_functions.py

def test_functions_w_keyword_arguments(self):
    self.assertEqual(
        src.functions.function_w_keyword_arguments(
            first_name='first_name',
            last_name='last_name',
        ),
        ('first_name', 'last_name')
    )

the terminal shows AttributeError

AttributeError: module 'src.functions' has no attribute 'function_w_keyword_arguments'. Did you mean: 'function_w_unknown_positional_arguments'?

green: make it pass

  • I add a function definition to functions.py

    def function_w_keyword_arguments():
        return None
    

    the terminal shows TypeError

    TypeError: function_w_keyword_arguments() got an unexpected keyword argument 'first_name'
    

    I add the argument to the defintion

    def function_w_keyword_arguments(first_name):
        return None
    

    the terminal shows TypeError

    TypeError: function_w_keyword_arguments() got an unexpected keyword argument 'last_name'. Did you mean 'first_name'?
    

    I add the argument

    def function_w_keyword_arguments(first_name, last_name):
        return None
    

    the terminal shows AssertionError

    AssertionError: None != (‘first_name’, ‘last_name’)

    I change the return statement

    def function_w_keyword_arguments(first_name, last_name):
        return ('first_name', 'last_name')
    

    the test passes

refactor: make it better

  • So far function_w_keyword_arguments looks the same as function_w_positional_arguments, I have not yet seen a difference between a positional argument and a keyword argument. I add an assertion that puts the input data out of order to see if there is a difference

    def test_functions_w_keyword_arguments(self):
        self.assertEqual(
            functions.function_w_keyword_arguments(
                first_name='first_name',
                last_name='last_name'
            ),
            ('first_name', 'last_name')
        )
        self.assertEqual(
            src.functions.function_w_keyword_arguments(
                last_name='last_name',
                first_name='first_name',
            ),
            ('last_name', 'first_name')
        )
    

    the terminal shows AssertionError

    AssertionError: Tuples differ: ('first_name', 'last_name') != ('last_name', 'first_name')
    

    the order stayed the same. I change the expectation to make the test pass

    self.assertEqual(
        src.functions.function_w_keyword_arguments(
            last_name='last_name',
            first_name='first_name',
        ),
        ('first_name', 'last_name')
    )
    

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

# ADD examples with dictionary as input


test_functions_w_unknown_keyword_arguments

  • The function currently only takes in 2 keyword arguments. What if I want a function that can take in any number of keyword arguments? There is a starred expression for keyword arguments - **. I add an assertion

    def test_functions_w_unknown_keyword_arguments(self):
        self.assertEqual(
            src.functions.function_w_unknown_keyword_arguments(
                a=1, b=2, c=3, d=4
            ),
            None
        )
    

    the terminal shows AttributeError

    AttributeError: module 'src.functions' has no attribute 'function_w_unknown_keyword_arguments'. Did you mean: 'function_w_keyword_arguments'?
    

    I add a function using a starred expression

    def function_w_unknown_keyword_arguments(*arguments):
        return arguments
    

    the terminal shows TypeError

    TypeError: function_w_unknown_keyword_arguments() got an unexpected keyword argument 'a'
    

    the starred expression for keyword arguments is different, I change the function

    def function_w_unknown_keyword_arguments(**keyword_arguments):
        return keyword_arguments
    

    the terminal shows AssertionError

    AssertionError: {'a': 1, 'b': 2, 'c': 3, 'd': 4} != None
    

    I change the expectation in the test to match

    def test_functions_w_unknown_keyword_arguments(self):
        self.assertEqual(
            src.functions.function_w_unknown_keyword_arguments(
                a=1, b=2, c=3, d=4
            ),
            {'a': 1, 'b': 2, 'c': 3, 'd': 4}
        )
    

    the test passes

  • I add another assertion with a different number of inputs

    self.assertEqual(
        src.functions.function_w_unknown_keyword_arguments(
            a=1, b=2, c=3, d=4
        ),
        {'a': 1, 'b': 2, 'c': 3, 'd': 4}
    )
    self.assertEqual(
        src.functions.function_w_unknown_keyword_arguments(
            none=None,
            a_boolean=bool,
            an_integer=int,
            a_float=float,
            a_string=str,
            a_tuple=tuple,
            a_list=list,
            a_set=set,
            a_dictionary=dict
        ),
        {}
    )
    

    the terminal shows AssertionError

    AssertionError: {'none': None, 'a_boolean': <class 'bool'>[190 chars]ct'>} != {}
    

    I change the expectation to match the values in the terminal

    self.assertEqual(
        src.functions.function_w_unknown_keyword_arguments(
            none=None,
            a_boolean=bool,
            an_integer=int,
            a_float=float,
            a_string=str,
            a_tuple=tuple,
            a_list=list,
            a_set=set,
            a_dictionary=dict
        ),
        dict(
            a_boolean=bool,
            a_dictionary=dict,
            a_float=float,
            a_list=list,
            a_set=set,
            a_string=str,
            a_tuple=tuple,
            an_integer=int,
            none=None,
        )
    )
    

    the test passes

# ADD examples with dictionary as input


test_functions_w_positional_and_keyword_arguments

red: make it fail

I can also define functions to take both positional arguments and keyword arguments as inputs. I add a new failing test to test_functions.py

def test_functions_w_positional_and_keyword_arguments(self):
    self.assertEqual(
    functions.takes_positional_and_keyword_arguments(
        last_name='last_name', 'first_name'
    ),
    {}
  )

the terminal shows a SyntaxError because I put a positional argument after a keyword argument. I add the error to the list of Exceptions encountered

# Exceptions Encountered
# AssertionError
# ModuleNotFoundError
# AttributeError
# TypeError
# SyntaxError

green: make it pass

  • I fix the order of arguments in test_functions_w_positional_and_keyword_arguments since keyword arguments come after positional arguments

    def test_functions_w_positional_and_keyword_arguments(self):
        self.assertEqual(
            functions.takes_positional_and_keyword_arguments(
                'first_name', last_name='last_name'
            ),
            {}
        )
    

    the terminal shows AttributeError

  • I add a definition for the function to functions.py

    def takes_positional_and_keyword_arguments():
        return None
    

    the terminal shows TypeError

    TypeError: takes_positional_and_keyword_arguments() got an unexpected keyword argument 'last_name'
    
  • I make the function signature to take in an argument

    def takes_positional_and_keyword_arguments(last_name):
        return None
    

    the terminal shows another TypeError

    TypeError: takes_positional_and_keyword_arguments() got multiple values for argument 'last_name'
    
  • I add another argument to the function signature

    def takes_positional_and_keyword_arguments(last_name, first_name):
        return None
    

    the terminal shows the same error even though I have 2 different arguments. I need a way to let the takes_positional_and_keyword_arguments know which argument is positional and which is a keyword argument

  • I reorder the arguments in the signature

    def takes_positional_and_keyword_arguments(first_name, last_name):
        return None
    

    the terminal shows AssertionError

  • I edit the return statement to make the test pass

    def takes_positional_and_keyword_arguments(first_name, last_name):
        return first_name, last_name
    

    the terminal changes the AssertionError with the values I just added

  • I make test_functions_w_positional_and_keyword_arguments to make the results match the expectation

    def test_functions_w_positional_and_keyword_arguments(self):
        self.assertEqual(
        functions.takes_positional_and_keyword_arguments(
                'first_name', last_name='last_name'
            ),
            ('first_name', 'last_name')
        )
    

    the terminal shows passing tests

refactor: make it better

Hold on a second. This looks exactly like what I did in test_functions_w_positional_arguments. I cannot tell from the function signature which argument is positional and which is a keyword argument and do not want to wait for the function to fail when I send in values to find out

  • I make the signature of takes_positional_and_keyword_arguments to have a default value for the keyword argument

    def takes_positional_and_keyword_arguments(first_name, last_name=None):
        return first_name, last_name
    

    all tests are still passing

  • I did not add a default argument for first_name, what would happen if I did?

    def takes_positional_and_keyword_arguments(first_name=None, last_name=None):
        return first_name, last_name
    

    I still have passing tests. It looks like Python lets us use default arguments with no issues, and I can provide keyword arguments positionally without using the name.

  • I add another test to test_functions_w_positional_and_keyword_arguments to show this

    def test_functions_w_positional_and_keyword_arguments(self):
        self.assertEqual(
            functions.takes_positional_and_keyword_arguments(
                'first_name', last_name='last_name'
            ),
            ('first_name', 'last_name')
        )
        self.assertEqual(
            functions.takes_positional_and_keyword_arguments(
                'first_name', 'last_name'
            ),
            ('first_name', 'last_name')
        )
    

    all the tests are still passing. The problem here is without the names the program is going to take the input data in the order I provide it so it is better to be explicit with the names, from the Zen of Python : Explicit is better than implicit.

  • I add 2 tests, this time for an unknown number of positional and keyword arguments

    def test_functions_w_positional_and_keyword_arguments(self):
        self.assertEqual(
            functions.takes_positional_and_keyword_arguments(
                'first_name', last_name='last_name'
            ),
            ('first_name', 'last_name')
        )
        self.assertEqual(
            functions.takes_positional_and_keyword_arguments(
                'first_name', 'last_name'
            ),
            ('first_name', 'last_name')
        )
        self.assertEqual(
            functions.takes_positional_and_keyword_arguments(),
            (None, None)
        )
        self.assertEqual(
            functions.takes_positional_and_keyword_arguments(
                bool, int, float, str, tuple, list, set, dict,
                a_boolean=bool, an_integer=int, a_float=float,
                a_string=str, a_tuple=tuple, a_list=list,
                a_set=set, a_dictionary=dict
            ),
            ()
        )
    

    the terminal shows TypeError because the function signature only has 2 keyword arguments which are not provided in the call

  • using what I know from previous tests I can alter the function to use starred expressions

    def takes_positional_and_keyword_arguments(*args, **kwargs):
        return args, kwargs
    

    the terminal shows AssertionError for a previous passing test. I have introduced a regression

    E   AssertionError: Tuples differ: (('first_name',), {'last_name': 'last_name'}) != ('first_name', 'last_name')
    
  • I comment out the other assertions so I can focus on the failing test

    def test_functions_w_positional_and_keyword_arguments(self):
        self.assertEqual(
          functions.takes_positional_and_keyword_arguments(
            'first_name', last_name='last_name'
          ),
          ('first_name', 'last_name')
        )
        # self.assertEqual(
        #    functions.takes_positional_and_keyword_arguments(
        #        'first_name', 'last_name'
        #    ),
        #    (('first_name', 'last_name'), {})
        # )
        # self.assertEqual(
        #     functions.takes_positional_and_keyword_arguments(),
        #     (None, None)
        # )
        # self.assertEqual(
        #    functions.takes_positional_and_keyword_arguments(
        #        bool, int, float, str, tuple, list, set, dict,
        #        a_boolean=bool, an_integer=int, a_float=float,
        #        a_string=str, a_tuple=tuple, a_list=list,
        #        a_set=set, a_dictionary=dict
        #    ),
        #    ()
        # )
    
  • I change the expected values in the test to make it pass

    self.assertEqual(
        functions.takes_positional_and_keyword_arguments(
            'first_name', last_name='last_name'
        ),
        (('first_name',), {'last_name': 'last_name'})
    )
    

    the terminal shows tests passing, with the positional argument in parentheses and the keyword argument in curly braces

  • I uncomment the next test

    self.assertEqual(
        functions.takes_positional_and_keyword_arguments(
            'first_name', 'last_name'
        ),
        (('first_name', 'last_name'), {})
    )
    

    the terminal shows AssertionError

    E    AssertionError: Tuples differ: (('first_name', 'last_name'), {}) != (('first_name', 'last_name'), {})
    
  • I make the test pass with both positional arguments in parentheses and empty curly braces since there are no keyword arguments

    self.assertEqual(
        functions.takes_positional_and_keyword_arguments(
            'first_name', 'last_name'
        ),
        (('first_name', 'last_name'), {})
    )
    

    and the terminal shows passing tests

  • I uncomment the next test to see it fail

    self.assertEqual(
        functions.takes_positional_and_keyword_arguments(),
        (None, None)
    )
    

    the terminal shows AssertionError

    AssertionError: Tuples differ: ((), {}) != (None, None)
    
  • I make the test pass with empty parentheses and curly braces as the expectation since no positional or keyword arguments were provided as inputs

    self.assertEqual(
        functions.takes_positional_and_keyword_arguments(),
        ((), {})
    )
    
  • I uncomment the last test to see it fail and the terminal shows AssertionError

    AssertionError: Tuples differ: ((<class 'bool'>, <class 'int'>, <class 'f[307 chars]t'>}) != ()
    
  • I make the test pass

    self.assertEqual(
        functions.takes_positional_and_keyword_arguments(
            bool, int, float, str, tuple, list, set, dict,
            a_boolean=bool, an_integer=int, a_float=float,
            a_string=str, a_tuple=tuple, a_list=list,
            a_set=set, a_dictionary=dict
        ),
        (
            (bool, int, float, str, tuple, list, set, dict,),
            {
                'a_boolean': bool,
                'an_integer': int,
                'a_float': float,
                'a_string': str,
                'a_tuple': tuple,
                'a_list': list,
                'a_set': set,
                'a_dictionary': dict
            }
        )
    )
    

review

the tests show that

  • I can define default values for arguments

  • positional arguments must come before keyword arguments

  • I can use **kwargs to represent any number of keyword arguments

  • keyword arguments are represented as dictionaries

  • I can use *args to represent any number of positional arguments

  • positional arguments are represented as tuples

  • identity functions return their input

  • constant functions always return the same thing

  • functions return None by default

  • functions are defined using the def keyword

Would you like to test classes?


functions: tests and solutions