how to make a person with exceptions


I want to be able to check if a person can vote, and if they can get a license. In other words, I want something in the person project to make decisions based on conditions. For example

  • If a person is younger than 18

    • the person cannot get a license.

    • the person cannot vote.

  • If a person is 18 or older

    • and passes a test, the person can get a license.

    • and is a citizen, the person can vote.


preview

I have these tests by the end of the chapter

person/tests/test_person.py
  1import datetime
  2import src.person
  3import unittest
  4
  5
  6class TestPerson(unittest.TestCase):
  7
  8    @staticmethod
  9    def calculate_age(year_of_birth):
 10        return (
 11            datetime.date.today().year
 12          - year_of_birth
 13        )
 14
 15    def test_joe(self):
 16        first_name = 'joe'
 17        last_name = 'blow'
 18        sex = 'M'
 19        year_of_birth = 1996
 20
 21        reality = src.person.factory(
 22            first_name=first_name,
 23            last_name=last_name,
 24            sex=sex,
 25            year_of_birth=year_of_birth,
 26        )
 27        my_expectation = (
 28            f'{first_name}, {last_name},'
 29            f' {sex}, {year_of_birth}'
 30        )
 31        assert reality == my_expectation
 32        self.assertEqual(reality, my_expectation)
 33
 34        reality = src.person.say_hello(
 35            first_name=first_name,
 36            last_name=last_name,
 37            year_of_birth=year_of_birth,
 38        )
 39        my_expectation = (
 40            f'Hello, my name is {first_name}'
 41            f' {last_name} and I am'
 42            f' {self.calculate_age(year_of_birth)}.'
 43        )
 44        assert reality == my_expectation
 45        self.assertEqual(reality, my_expectation)
 46
 47        joe = src.person.Person(
 48            first_name=first_name,
 49            last_name=last_name,
 50            sex=sex,
 51            year_of_birth=year_of_birth,
 52        )
 53
 54        reality = joe.say_hello()
 55        assert reality == my_expectation
 56        self.assertEqual(reality, my_expectation)
 57        self.assertEqual(joe.can_vote(), True)
 58        self.assertEqual(joe.can_get_license(), False)
 59
 60    def test_jane(self):
 61        first_name = 'jane'
 62        last_name = 'doe'
 63        sex = 'F'
 64        year_of_birth = 1991
 65
 66        reality = src.person.factory(
 67            first_name=first_name,
 68            last_name=last_name,
 69            sex=sex,
 70            year_of_birth=year_of_birth,
 71        )
 72        my_expectation = (
 73            f'{first_name}, {last_name},'
 74            f' {sex}, {year_of_birth}'
 75        )
 76        assert reality == my_expectation
 77        self.assertEqual(reality, my_expectation)
 78
 79        reality = src.person.say_hello(
 80            first_name=first_name,
 81            last_name=last_name,
 82            year_of_birth=year_of_birth,
 83        )
 84        my_expectation = (
 85            f'Hello, my name is {first_name}'
 86            f' {last_name} and I am'
 87            f' {self.calculate_age(year_of_birth)}.'
 88        )
 89        assert reality == my_expectation
 90        self.assertEqual(reality, my_expectation)
 91
 92        jane = src.person.Person(
 93            first_name=first_name,
 94            last_name=last_name,
 95            sex=sex,
 96            year_of_birth=year_of_birth,
 97            passed_test=True,
 98        )
 99
100        reality = jane.say_hello()
101        assert reality == my_expectation
102        self.assertEqual(reality, my_expectation)
103        self.assertEqual(jane.can_vote(), True)
104        self.assertEqual(jane.can_get_license(), True)
105
106    def test_john(self):
107        first_name = 'john'
108        last_name = 'smith'
109        sex = 'M'
110        year_of_birth = 1980
111        # year_of_birth = 1580
112        # raises AssertionError
113        # because older than 120
114
115        reality = src.person.factory(
116            first_name=first_name,
117            last_name=last_name,
118            sex=sex,
119            year_of_birth=year_of_birth,
120        )
121        my_expectation = (
122            f'{first_name}, {last_name},'
123            f' {sex}, {year_of_birth}'
124        )
125        assert reality == my_expectation
126        self.assertEqual(reality, my_expectation)
127
128        reality = src.person.say_hello(
129            first_name=first_name,
130            last_name=last_name,
131            year_of_birth=year_of_birth,
132        )
133        my_expectation = (
134            f'Hello, my name is {first_name}'
135            f' {last_name} and I am'
136            f' {self.calculate_age(year_of_birth)}.'
137        )
138        assert reality == my_expectation
139        self.assertEqual(reality, my_expectation)
140
141        john = src.person.Person(
142            first_name=first_name,
143            last_name=last_name,
144            sex=sex,
145            year_of_birth=year_of_birth,
146            is_citizen=False,
147        )
148
149        reality = john.say_hello()
150        assert reality == my_expectation
151        self.assertEqual(reality, my_expectation)
152        self.assertEqual(john.can_vote(), False)
153        self.assertEqual(john.can_get_license(), False)
154
155    def test_mary(self):
156        first_name = 'mary'
157        last_name = 'public'
158        sex = 'F'
159        year_of_birth = 2000
160
161        reality = src.person.factory(
162            first_name=first_name,
163            last_name=last_name,
164            sex=sex,
165            year_of_birth=year_of_birth,
166        )
167        my_expectation = (
168            f'{first_name}, {last_name},'
169            f' {sex}, {year_of_birth}'
170        )
171        assert reality == my_expectation
172        self.assertEqual(reality, my_expectation)
173
174        reality = src.person.say_hello(
175            first_name=first_name,
176            last_name=last_name,
177            year_of_birth=year_of_birth,
178        )
179        my_expectation = (
180            f'Hello, my name is {first_name}'
181            f' {last_name} and I am'
182            f' {self.calculate_age(year_of_birth)}.'
183        )
184        assert reality == my_expectation
185        self.assertEqual(reality, my_expectation)
186
187        mary = src.person.Person(
188            first_name=first_name,
189            last_name=last_name,
190            sex=sex,
191            year_of_birth=year_of_birth,
192            is_citizen=False,
193            passed_test=True,
194        )
195
196        reality = mary.say_hello()
197        assert reality == my_expectation
198        self.assertEqual(reality, my_expectation)
199        self.assertEqual(mary.can_vote(), False)
200        self.assertEqual(mary.can_get_license(), True)
201
202    def test_underage_citizen(self):
203        person = src.person.Person(
204            first_name='first_name',
205            last_name='last_name',
206            sex='M',
207            year_of_birth=datetime.date.today().year-17,
208            is_citizen=True,
209            passed_test=True,
210        )
211        self.assertEqual(person.can_vote(), False)
212        self.assertEqual(
213            person.can_get_license(), False
214        )
215
216    @unittest.skip('will always fail')
217    def test_when_year_of_birth_is_not_an_integer(self):
218        person = src.person.Person(
219            first_name='first_name',
220            last_name='last_name',
221            sex='M',
222            # year_of_birth=None,    # fails
223            # year_of_birth=False,   # fails
224            # year_of_birth=2026.0,  # fails
225            # year_of_birth='2026',  # fails
226            # year_of_birth=(2026,), # fails
227        )
228        # person.say_hello()
229        # fails if year_of_birth is not an integer
230
231    def test_dir_person_class(self):
232        reality = dir(src.person.Person)
233        my_expectation = [
234            '__class__',
235            '__delattr__',
236            '__dict__',
237            '__dir__',
238            '__doc__',
239            '__eq__',
240            '__firstlineno__',
241            '__format__',
242            '__ge__',
243            '__getattribute__',
244            '__getstate__',
245            '__gt__',
246            '__hash__',
247            '__init__',
248            '__init_subclass__',
249            '__le__',
250            '__lt__',
251            '__module__',
252            '__ne__',
253            '__new__',
254            '__reduce__',
255            '__reduce_ex__',
256            '__repr__',
257            '__setattr__',
258            '__sizeof__',
259            '__static_attributes__',
260            '__str__',
261            '__subclasshook__',
262            '__weakref__',
263            'can_get_license',
264            'can_vote',
265            'check_age',
266            'say_hello',
267        ]
268        assert reality == my_expectation
269        self.assertEqual(reality, my_expectation)
270
271    def test_dir_person_instance(self):
272        an_instance_of_person = src.person.Person(
273            first_name='first_name',
274            last_name='last_name',
275            sex='M',
276            year_of_birth=2026,
277        )
278
279        reality = dir(an_instance_of_person)
280        my_expectation = [
281            '__class__',
282            '__delattr__',
283            '__dict__',
284            '__dir__',
285            '__doc__',
286            '__eq__',
287            '__firstlineno__',
288            '__format__',
289            '__ge__',
290            '__getattribute__',
291            '__getstate__',
292            '__gt__',
293            '__hash__',
294            '__init__',
295            '__init_subclass__',
296            '__le__',
297            '__lt__',
298            '__module__',
299            '__ne__',
300            '__new__',
301            '__reduce__',
302            '__reduce_ex__',
303            '__repr__',
304            '__setattr__',
305            '__sizeof__',
306            '__static_attributes__',
307            '__str__',
308            '__subclasshook__',
309            '__weakref__',
310            'age',
311            'can_get_license',
312            'can_vote',
313            'check_age',
314            'first_name',
315            'is_citizen',
316            'last_name',
317            'passed_test',
318            'say_hello',
319            'sex',
320            'year_of_birth',
321        ]
322        assert reality == my_expectation
323        self.assertEqual(reality, my_expectation)
324
325
326# Exceptions seen
327# AssertionError
328# NameError
329# TypeError
330# AttributeError
331# SyntaxError

open the project

  • I open a terminal

  • I change directory to the project

    cd person
    
  • I open test_person.py from the tests folder

  • I use pytest-watcher to run the tests automatically

    uv run pytest-watcher . --now
    

    the terminal shows

    tests/test_person.py .......                        [100%]
    
    =================== 7 passed in P.QRs ====================
    

add can_vote method

RED: make it fail


I add a call to can_vote from test_joe

54        reality = joe.say_hello()
55        assert reality == my_expectation
56        self.assertEqual(reality, my_expectation)
57        self.assertEqual(joe.can_vote(), True)
58
59    def test_jane(self):

the terminal is my friend, and shows AttributeError

AttributeError: 'Person' object
                has no attribute 'can_vote'

GREEN: make it pass


  • I open person/__init__.py from the src folder

  • I add a function definition to the Person class in person.py

     4  class Person:
     5
     6      def __init__(
     7          self, first_name, last_name,
     8          sex, year_of_birth=None,
     9      ):
    10          self.first_name = first_name
    11          self.last_name = last_name
    12          self.year_of_birth = year_of_birth
    13          self.sex = sex
    14
    15      def can_vote():
    16          return True
    17
    18      def say_hello(self):
    19          return (
    20              f'Hello, my name is {self.first_name}'
    21              f' {self.last_name} and I am'
    22              f' {calculate_age(self.year_of_birth)}.'
    23          )
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.can_vote() takes
               0 positional arguments but 1 was given
    
  • I add the staticmethod decorator

    15    @staticmethod
    16    def can_vote():
    17        return True
    18
    19    def say_hello(self):
    

    the terminal is my friend, and shows AssertionError

    FAILED ...::TestPerson::test_dir_person_class -
        AssertionError: assert
            ['__class__',...'__eq__', ...] ==...
    FAILED ...::TestPerson::test_dir_person_instance -
        AssertionError: assert
            ['__class__',...'__eq__', ...] ==...
    

    the tests for the attributes and methods of the Person class and an instance of it are failing because I added a method to it.

  • I add can_vote to test_dir_person_class

    237            'can_vote',
    238            'say_hello',
    239        ]
    240        assert reality == my_expectation
    241        self.assertEqual(reality, my_expectation)
    242
    243    def test_dir_person_instance(self):
    
  • I add can_vote to test_dir_person_instance

    282            'can_vote',
    283            'first_name',
    284            'last_name',
    285            'say_hello',
    286            'sex',
    287            'year_of_birth',
    288        ]
    289        assert reality == my_expectation
    290        self.assertEqual(reality, my_expectation)
    291
    292
    293# Exceptions seen
    

    the test passes.

    These tests are good because they help document what is in the class and catch its changes immediately.

    They are a problem because class attributes can change between Python versions, I have to remember the correct order of names and I am keeping two lists. There has to be a better way.

  • I open a new terminal then make sure I am in the person folder

    cd person
    
  • I add a git commit message in the new terminal

    git commit -am 'add can_vote method'
    

add is_citizen attribute

I want can_vote to return

  • False for no the person cannot vote if the person is not a citizen.

  • True for yes the person can vote if the person is a citizen.


RED: make it fail


  • I go back to the terminal where the tests are running

  • I add a call to can_vote from test_jane

     98        reality = jane.say_hello()
     99        assert reality == my_expectation
    100        self.assertEqual(reality, my_expectation)
    101        self.assertEqual(jane.can_vote(), True)
    102
    103    def test_john(self):
    

    the test is still green.

  • I add a call to can_vote from test_john

    145        reality = john.say_hello()
    146        assert reality == my_expectation
    147        self.assertEqual(reality, my_expectation)
    148        self.assertEqual(john.can_vote(), False)
    149
    150    def test_mary(self):
    

    the terminal is my friend, and shows AssertionError

    AssertionError: True != False
    

    The can_vote method has to make a decision based on something.


GREEN: make it pass


  • I add is_citizen to the call to the Person class for john

    138        john = src.person.Person(
    139            first_name=first_name,
    140            last_name=last_name,
    141            sex=sex,
    142            year_of_birth=year_of_birth,
    143            is_citizen=False,
    144        )
    145
    146        reality = john.say_hello()
    147        assert reality == my_expectation
    148        self.assertEqual(reality, my_expectation)
    149        self.assertEqual(john.can_vote(), False)
    150
    151    def test_mary(self):
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.__init__() got
               an unexpected keyword argument
               'is_citizen'
    

    because the definition for the __init__ method only takes five inputs (self, first_name, last_name, sex and year_of_birth) and I called it with is_citizen which is not one of those names.

  • I add is_citizen to the parentheses of the __init__ method, in person.py

     4class Person:
     5
     6    def __init__(
     7        self, first_name, last_name,
     8        sex, year_of_birth=None,
     9        is_citizen,
    10    ):
    

    the terminal is my friend, and shows SyntaxError

    SyntaxError: parameter without a default
         follows parameter with a default
    

    because parameters without default values must come before parameters with default values.

  • I give is_citizen a value to make it optional

     4class Person:
     5
     6    def __init__(
     7        self, first_name, last_name,
     8        sex, year_of_birth=None,
     9        # is_citizen,
    10        is_citizen=True,
    11    ):
    

    the terminal goes back to the AssertionError.

  • I add an instance attribute for is_citizen so I can use it in the can_vote method

     4class Person:
     5
     6    def __init__(
     7        self, first_name, last_name,
     8        sex, year_of_birth=None,
     9        # is_citizen,
    10        is_citizen=True,
    11    ):
    12        self.first_name = first_name
    13        self.last_name = last_name
    14        self.year_of_birth = year_of_birth
    15        self.sex = sex
    16        self.is_citizen = is_citizen
    17
    18    @staticmethod
    

    still AssertionError.

  • I use the class attribute in the can_vote method

    18    @staticmethod
    19    def can_vote():
    20        # return True
    21        return self.is_citizen
    22
    23    def say_hello(self):
    

    the terminal is my friend, and shows NameError

    NameError: name 'self' is not defined
    
  • I remove the staticmethod decorator from the can_vote method then add self to the parentheses

    18    # @staticmethod
    19    # def can_vote():
    20    def can_vote(self):
    21        # return True
    22        return self.is_citizen
    23
    24    def say_hello(self):
    

    the terminal is my friend, and shows AssertionError for test_dir_person_instance because I added a new attribute (is_citizen).

  • I add is_citizen to my_expectation in test_dir_person_instance in test_person.py

    285            'can_vote',
    286            'first_name',
    287            'is_citizen',
    288            'last_name',
    289            'say_hello',
    290            'sex',
    291            'year_of_birth',
    292        ]
    293        assert reality == my_expectation
    294        self.assertEqual(reality, my_expectation)
    295
    296
    297# Exceptions seen
    

    the test passes.

    joe and jane do not need to pass a value for the is_citizen parameter because a method uses the default value for a parameter when it is called without the parameter.


REFACTOR: make it better


  • I remove the commented lines from person.py

     4class Person:
     5
     6    def __init__(
     7        self, first_name, last_name,
     8        sex, year_of_birth=None,
     9        is_citizen=True,
    10    ):
    11        self.first_name = first_name
    12        self.last_name = last_name
    13        self.year_of_birth = year_of_birth
    14        self.sex = sex
    15        self.is_citizen = is_citizen
    16
    17    def can_vote(self):
    18        return self.is_citizen
    19
    20    def say_hello(self):
    
  • I add a call to can_vote from test_mary in test_person.py

    190        reality = mary.say_hello()
    191        assert reality == my_expectation
    192        self.assertEqual(reality, my_expectation)
    193        self.assertEqual(mary.can_vote(), False)
    194
    195    def test_when_year_of_birth_is_not_an_integer(self):
    

    the terminal is my friend, and shows AssertionError

    AssertionError: True != False
    

    because a method uses the default value for a parameter when it is called without the parameter.

  • I add is_citizen to the call to the Person class for mary

    183          mary = src.person.Person(
    184              first_name=first_name,
    185              last_name=last_name,
    186              sex=sex,
    187              year_of_birth=year_of_birth,
    188              is_citizen=False,
    189          )
    190
    191          reality = mary.say_hello()
    192          assert reality == my_expectation
    193          self.assertEqual(reality, my_expectation)
    194          self.assertEqual(mary.can_vote(), False)
    195
    196      def test_when_year_of_birth_is_not_an_integer(self):
    

    the test passes.

  • I add a git commit message in the other terminal

    git commit -am \
    'add is_citizen attribute'
    

add condition to can_vote

I want the can_vote method to use two conditions to make a decision

  • is the person a citizen?

  • is the person younger than 18?

I can do that with an if statement


RED: make it fail


  • I go back to the terminal where the tests are running

  • I add a test for a person who is a citizen and younger than 18

    191        reality = mary.say_hello()
    192        assert reality == my_expectation
    193        self.assertEqual(reality, my_expectation)
    194        self.assertEqual(mary.can_vote(), False)
    195
    196    def test_underage_citizen(self):
    197        person = src.person.Person(
    198            first_name='first_name',
    199            last_name='last_name',
    200            sex='M',
    201            year_of_birth=datetime.date.today().year-17,
    202            is_citizen=True,
    203        )
    204        self.assertEqual(person.can_vote(), False)
    205
    206    def test_when_year_of_birth_is_not_an_integer(self):
    

    the terminal is my friend, and shows AssertionError

    AssertionError: True != False
    
    • because can_vote returns the value of is_citizen, it does not care about the age of the person.

    • I use a calculation (datetime.date.today().year-17) as the year of birth so that the person will always be younger than 18 in any year the test is run.


GREEN: make it pass


  • I add an if statement with a call to the calculate_age function from the can_vote method in person.py

    17    def can_vote(self):
    18        age = calculate_age(self.year_of_birth)
    19        if age < 18:
    20            return False
    21        return self.is_citizen
    22
    23    def say_hello(self):
    

    the test passes because this happens when if age < 18: runs, Python checks if age which is the result of calculate_age(self.year_of_birth) is less than 18

  • If age is greater than or equal to 18, it leaves the if statement and continues to run the rest of the method - return self.is_citizen, which returns

    • False as the output if the person is not a citizen

      self.is_citizen = False
      age >= 18
      
      person.can_vote
      └──def can_vote(self):
         ├── if age < 18:
               return False
         └── return self.is_citizen
      
    • True as the output, if the person is a citizen

      self.is_citizen = True
      age >= 18
      
      person.can_vote
      └──def can_vote(self):
          ├── if age < 18:
                return False
          └── return self.is_citizen
      

    then leaves the function since the return statement is the last thing to run in a function.

  • If age is less than 18, it goes to the next line - return False, which returns False as the output, then leaves the function since the return statement is the last thing to run in a function.

    self.is_citizen = False
    age < 18
    
    person.can_vote
    └──def can_vote(self):
       └── if age < 18:
           └── return False
           return self.is_citizen
    
    self.is_citizen = True
    age < 18
    
    person.can_vote
    └──def can_vote(self):
       └── if age < 18:
           └── return False
           return self.is_citizen
    
  • I add a git commit message in the other terminal

    git commit -am \
    'add condition to can_vote'
    

add can_get_license method

RED: make it fail


I add a call to can_get_license from test_joe

54        reality = joe.say_hello()
55        assert reality == my_expectation
56        self.assertEqual(reality, my_expectation)
57        self.assertEqual(joe.can_vote(), True)
58        self.assertEqual(joe.can_get_license(), False)
59
60    def test_jane(self):

the terminal is my friend, and shows AttributeError

AttributeError: 'Person' object
                has no attribute 'can_get_license'

GREEN: make it pass


  • I add a method definition to the Person class in person.py

     4class Person:
     5
     6    def __init__(
     7        self, first_name, last_name,
     8        sex, year_of_birth=None,
     9        is_citizen=True,
    10    ):
    11        self.first_name = first_name
    12        self.last_name = last_name
    13        self.year_of_birth = year_of_birth
    14        self.sex = sex
    15        self.is_citizen = is_citizen
    16
    17    def can_get_license():
    18        return False
    19
    20    def can_vote(self):
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.can_get_license()
               takes 0 positional arguments but 1 was given
    
  • I add the staticmethod decorator

    17    @staticmethod
    18    def can_get_license():
    19        return False
    20
    21    def can_vote(self):
    

    the terminal is my friend, and shows AssertionError for test_dir_person_class and test_dir_person_instance.

  • I add can_get_license to test_dir_person_class in test_person.py

    254            'can_get_license',
    255            'can_vote',
    256            'say_hello'
    257        ]
    258        assert reality == my_expectation
    259        self.assertEqual(reality, my_expectation)
    260
    261    def test_dir_person_instance(self):
    
  • I add can_get_license to test_dir_person_instance

    299            'can_get_license',
    300            'can_vote',
    301            'first_name',
    302            'is_citizen',
    303            'last_name',
    304            'say_hello',
    305            'sex',
    306            'year_of_birth',
    307        ]
    308        assert reality == my_expectation
    309        self.assertEqual(reality, my_expectation)
    310
    311
    312# Exceptions seen
    

    the test passes.

  • I add a git commit message in the other terminal

    git commit -am \
    'add can_get_license method'
    

add passed_test attribute

I want can_get_license to return

  • False for no the person cannot get a license if the person did not pass the test.

  • True for yes the person can get a license if the person passed the test.


RED: make it fail


I add a call to can_get_license from test_mary

192        reality = mary.say_hello()
193        assert reality == my_expectation
194        self.assertEqual(reality, my_expectation)
195        self.assertEqual(mary.can_vote(), False)
196        self.assertEqual(mary.can_get_license(), True)
197
198    def test_underage_citizen(self):

the terminal is my friend, and shows AssertionError

AssertionError: False != True

GREEN: make it pass


  • I add passed_test to the call to the Person class for mary

    184        mary = src.person.Person(
    185            first_name=first_name,
    186            last_name=last_name,
    187            sex=sex,
    188            year_of_birth=year_of_birth,
    189            is_citizen=False,
    190            passed_test=True,
    191        )
    192
    193        reality = mary.say_hello()
    194        assert reality == my_expectation
    195        self.assertEqual(reality, my_expectation)
    196        self.assertEqual(mary.can_vote(), False)
    197        self.assertEqual(mary.can_get_license(), True)
    198
    199    def test_underage_citizen(self):
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.__init__() got
               an unexpected keyword argument
               'passed_test'
    

    because the definition for the __init__ method only takes six inputs (self, first_name, last_name, sex, year_of_birth and is_citizen). I called it with passed_test which is not one of those names.

  • I add passed_test to the parentheses of the __init__ method, in person.py

     4class Person:
     5
     6    def __init__(
     7        self, first_name, last_name,
     8        sex, year_of_birth=None,
     9        is_citizen=True,
    10        passed_test,
    11    ):
    

    the terminal is my friend, and shows SyntaxError

    SyntaxError: parameter without a default
         follows parameter with a default
    

    because parameters without default values must come before parameters with default values.

  • I give passed_test a value to make it optional

     4class Person:
     5
     6    def __init__(
     7        self, first_name, last_name,
     8        sex, year_of_birth=None,
     9        is_citizen=True,
    10        # passed_test,
    11        passed_test=False,
    12    ):
    

    the terminal goes back to the AssertionError.

  • I add an instance attribute for passed_test so I can use it in the can_get_license method

     4class Person:
     5
     6    def __init__(
     7        self, first_name, last_name,
     8        sex, year_of_birth=None,
     9        is_citizen=True,
    10        # passed_test,
    11        passed_test=False,
    12    ):
    13        self.first_name = first_name
    14        self.last_name = last_name
    15        self.year_of_birth = year_of_birth
    16        self.sex = sex
    17        self.is_citizen = is_citizen
    18        self.passed_test = passed_test
    19
    20    @staticmethod
    

    the terminal still shows AssertionError.

  • I use self.passed_test in the can_get_license method

    20    @staticmethod
    21    def can_get_license():
    22        # return False
    23        return self.passed_test
    24
    25    def can_vote(self):
    

    the terminal is my friend, and shows NameError

    NameError: name 'self' is not defined
    
  • I remove the staticmethod decorator from the can_get_license method then add self to the parentheses

    20    # @staticmethod
    21    # def can_get_license():
    22    def can_get_license(self):
    23        # return False
    24        return self.passed_test
    25
    26    def can_vote(self):
    

    the terminal is my friend, and shows AssertionError for test_dir_person_instance because I added a new attribute (passed_test).

  • I add passed_test to my_expectation in test_dir_person_instance in test_person.py

    301            'can_get_license',
    302            'can_vote',
    303            'first_name',
    304            'is_citizen',
    305            'last_name',
    306            'passed_test',
    307            'say_hello',
    308            'sex',
    309            'year_of_birth',
    310        ]
    311        assert reality == my_expectation
    312        self.assertEqual(reality, my_expectation)
    313
    314
    315# Exceptions seen
    

    the test passes.


REFACTOR: make it better


  • I remove the commented lines from person.py

     4class Person:
     5
     6    def __init__(
     7        self, first_name, last_name,
     8        sex, year_of_birth=None,
     9        is_citizen=True,
    10        passed_test=False,
    11    ):
    12        self.first_name = first_name
    13        self.last_name = last_name
    14        self.year_of_birth = year_of_birth
    15        self.sex = sex
    16        self.is_citizen = is_citizen
    17        self.passed_test = passed_test
    18
    19    def can_get_license(self):
    20        return self.passed_test
    21
    22    def can_vote(self):
    
  • I add a call to can_get_license from test_john, in test_person.py

    147        reality = john.say_hello()
    148        assert reality == my_expectation
    149        self.assertEqual(reality, my_expectation)
    150        self.assertEqual(john.can_vote(), False)
    151        self.assertEqual(john.can_get_license(), False)
    152
    153    def test_mary(self):
    

    the test passes because a method uses the default value for a parameter when it is called without the parameter.

  • I add a call to can_get_license from test_jane, in test_person.py

     99        reality = jane.say_hello()
    100        assert reality == my_expectation
    101        self.assertEqual(reality, my_expectation)
    102        self.assertEqual(jane.can_vote(), True)
    103        self.assertEqual(jane.can_get_license(), True)
    104
    105    def test_john(self):
    

    the terminal is my friend, and shows AssertionError

    AssertionError: False != True
    

    because a method uses the default value for a parameter when it is called without the parameter.

  • I add passed_test to the call to the Person class for jane

     92        jane = src.person.Person(
     93            first_name=first_name,
     94            last_name=last_name,
     95            sex=sex,
     96            year_of_birth=year_of_birth,
     97            passed_test=True,
     98        )
     99
    100        reality = jane.say_hello()
    101        assert reality == my_expectation
    102        self.assertEqual(reality, my_expectation)
    103        self.assertEqual(jane.can_vote(), True)
    104        self.assertEqual(jane.can_get_license(), True)
    105
    106    def test_john(self):
    

    the test passes.

  • I add a git commit message in the other terminal

    git commit -am \
    'add passed_test attribute'
    

john and joe do not need to pass a value for the passed_test parameter because a method uses the default value for a parameter when it is called without the parameter.


add condition to can_get_license

I want the can_get_license method to use two conditions to make a decision

  • did the person pass the test?

  • is the person 18 or older?


RED: make it fail


  • I go back to the terminal where the tests are running

  • I add an assertion to test_underage_citizen for a person who is younger than 18 and passed the test

    202    def test_underage_citizen(self):
    203        person = src.person.Person(
    204            first_name='first_name',
    205            last_name='last_name',
    206            sex='M',
    207            year_of_birth=datetime.date.today().year-17,
    208            is_citizen=True,
    209            passed_test=True,
    210        )
    211        self.assertEqual(person.can_vote(), False)
    212        self.assertEqual(
    213            person.can_get_license(), False
    214        )
    215
    216    def test_when_year_of_birth_is_not_an_integer(self):
    

    the terminal is my friend, and shows AssertionError

    AssertionError: True != False
    

    because can_get_license currently returns the value of passed_test. It does not care about the age of the person.


GREEN: make it pass


I add an if statement with a call to the calculate_age function from the can_get_license method in person.py

19    def can_get_license(self):
20        age = calculate_age(self.year_of_birth)
21        if age < 18:
22            return False
23        return self.passed_test
24
25    def can_vote(self):

the test passes because this happens when if age < 18: runs, Python checks if age which is the result of calculate_age(self.year_of_birth) is less than 18

  • If age is greater than or equal to 18, it leaves the if statement and continues to run the rest of the method - return self.passed_test, which returns

    • False as the output, if the person failed the test

      self.passed_test = False
      age >= 18
      
      person.can_get_license
      └──def can_get_license(self):
         ├── if age < 18:
               return False
         └── return self.passed_test
      
    • True as the output, if the person passed the test

      self.passed_test = True
      age >= 18
      
      person.can_get_license
      └──def can_get_license(self):
         ├── if age < 18:
               return False
         └── return self.passed_test
      

    then leaves the function since the return statement is the last thing to run in a function.

  • If age is less than 18, it goes to the next line - return False, which returns False as the output, then leaves the function since the return statement is the last thing to run in a function.

    self.passed_test = False
    age < 18
    
    person.can_get_license
    └──def can_get_license(self):
       └── if age < 18:
           └── return False
           return self.passed_test
    
    self.passed_test = True
    age < 18
    
    person.can_get_license
    └──def can_get_license(self):
       └── if age < 18:
           └── return False
           return self.passed_test
    
  • I add a git commit message in the other terminal

    git commit -am \
    'add condition to can_get_license'
    

extract age instance attribute

Three of the methods of the Person class call the calculate_age function.


RED: make it fail


I add an instance attribute to the __init__ method so that the age is calculated once when an instance is made, not every time one of the methods is called.

 4class Person:
 5
 6    def __init__(
 7        self, first_name, last_name,
 8        sex, year_of_birth=None,
 9        is_citizen=True,
10        passed_test=False,
11    ):
12        self.first_name = first_name
13        self.last_name = last_name
14        self.year_of_birth = year_of_birth
15        self.sex = sex
16        self.is_citizen = is_citizen
17        self.passed_test = passed_test
18        self.age = calculate_age(year_of_birth)
19
20    def can_get_license(self):

the terminal is my friend, and shows AssertionError

FAILED ...::TestPerson::test_dir_person_instance -
    AssertionError: assert ['__class__',...'__eq__', ...]
                        == ['__class__',...'__eq__', ...]
FAILED ...::TestPerson::
    test_when_year_of_birth_is_not_an_integer -
        AssertionError

how to skip a test

I can use `unittest.skip decorator`_ to skip a test. The problem with this solution is that I no longer know if the program does the thing the skipped test was written for.


GREEN: make it pass


  • I add the `unittest.skip decorator`_ to test_when_year_of_birth_is_not_an_integer with a note that it will always fail since it uses a year of birth that is not an integer, in test_person.py

    216    @unittest.skip('will always fail')
    217    def test_when_year_of_birth_is_not_an_integer(self):
    218        person = src.person.Person(
    219            first_name='first_name',
    220            last_name='last_name',
    221            sex='M',
    222            # year_of_birth=None,    # fails
    223            # year_of_birth=False,   # fails
    224            # year_of_birth=2026.0,  # fails
    225            # year_of_birth='2026',  # fails
    226            # year_of_birth=(2026,), # fails
    227        )
    228        # person.say_hello()
    229        # fails if year_of_birth is not an integer
    230
    231    def test_dir_person_class(self):
    

    the terminal is my friend, and shows AssertionError

    ================ short test summary info =================
    FAILED ...::TestPerson::test_dir_person_instance -
        AssertionError: assert ['__class__',...'__eq__', ...]
                            == ['__class__',...'__eq__', ...]
    ======== 1 failed, 6 passed, 1 skipped in S.TUs ==========
    
  • I add age to my_expectation in test_dir_person_instance

    309            'age',
    310            'can_get_license',
    311            'can_vote',
    312            'first_name',
    313            'is_citizen',
    314            'last_name',
    315            'passed_test',
    316            'say_hello',
    317            'sex',
    318            'year_of_birth',
    319        ]
    320        assert reality == my_expectation
    321        self.assertEqual(reality, my_expectation)
    322
    323
    324# Exceptions seen
    

    the test passes.


REFACTOR: make it better


  • I use self.age in the can_get_license method in person.py

    20    def can_get_license(self):
    21        # age = calculate_age(self.year_of_birth)
    22        # if age < 18:
    23        if self.age < 18:
    24            return False
    25        return self.passed_test
    26
    27    def can_vote(self):
    

    the tests are still green.

  • I use self.age in the can_vote method

    27    def can_vote(self):
    28        # age = calculate_age(self.year_of_birth)
    29        # if age < 18:
    30        if self.age < 18:
    31            return False
    32        return self.is_citizen
    33
    34    def say_hello(self):
    

    still green.

  • I use self.age in the say_hello method

    34    def say_hello(self):
    35        return (
    36            f'Hello, my name is {self.first_name}'
    37            f' {self.last_name} and I am'
    38            # f' {calculate_age(self.year_of_birth)}.'
    39            f' {self.age}.'
    40        )
    41
    42
    43def calculate_age(year_of_birth):
    

    green.

  • I remove the commented line from the say_hello method

    34    def say_hello(self):
    35        return (
    36            f'Hello, my name is {self.first_name}'
    37            f' {self.last_name} and I am'
    38            f' {self.age}.'
    39        )
    40
    41
    42def calculate_age(year_of_birth):
    
  • I add a git commit message in the other terminal

    git commit -am \
    'extract age instance attribute'
    

extract check_age method

can_get_license and can_vote look the same, they both

  • return False if self.age is less than 18

  • return something else if self.age is NOT less than 18


RED: make it fail


I add a method to the Person class that checks if the age is less than 18 and returns something else if it is not

27    def can_vote(self):
28        # age = calculate_age(self.year_of_birth)
29        # if age < 18:
30        if self.age < 18:
31            return False
32        return self.is_citizen
33
34    def check_age(age, response):
35        if age < 18:
36            return False
37        return response
38
39    def say_hello(self):

the terminal is my friend, and shows AssertionError for test_dir_person_class and test_dir_person_instance because I added a new method.


GREEN: make it pass


  • I add check_age to my_expectation in test_dir_person_class in test_person.py

    263            'can_get_license',
    264            'can_vote',
    265            'check_age',
    266            'say_hello',
    267        ]
    268        assert reality == my_expectation
    269        self.assertEqual(reality, my_expectation)
    270
    271    def test_dir_person_instance(self):
    
  • I add check_age to my_expectation in test_dir_person_instance

    310            'age',
    311            'can_get_license',
    312            'can_vote',
    313            'check_age',
    314            'first_name',
    315            'is_citizen',
    316            'last_name',
    317            'passed_test',
    318            'say_hello',
    319            'sex',
    320            'year_of_birth',
    321        ]
    322        assert reality == my_expectation
    323        self.assertEqual(reality, my_expectation)
    324
    325
    326# Exceptions seen
    327# AssertionError
    328# NameError
    329# TypeError
    330# AttributeError
    331# SyntaxError
    

    the test passes.


REFACTOR: make it better


  • I call the check_age method from the can_vote method in person.py

    27    def can_vote(self):
    28        # age = calculate_age(self.year_of_birth)
    29        # if age < 18:
    30        # if self.age < 18:
    31        #     return False
    32        # return self.is_citizen
    33        return self.check_age(
    34            self.age, self.is_citizen
    35        )
    36
    37    def check_age(age, response):
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.check_age() takes
               2 positional arguments but 3 were given
    

    because a method of an instance takes the instance of the class (self) it belongs to as the first argument.

  • I add the staticmethod decorator to the check_age method since it does not use things from the Person class, only what it receives as input

    37    @staticmethod
    38    def check_age(age, response):
    39        if age < 18:
    40            return False
    41        return response
    42
    43    def say_hello(self):
    

    the test passes.

  • I remove the commented lines from the can_vote method

    27    def can_vote(self):
    28        return self.check_age(
    29            self.age, self.is_citizen
    30        )
    31
    32    @staticmethod
    33    def check_age(age, response):
    
  • I call the check_age method from the can_get_license method

    20    def can_get_license(self):
    21        # age = calculate_age(self.year_of_birth)
    22        # if age < 18:
    23        # if self.age < 18:
    24        #     return False
    25        # return self.passed_test
    26        return self.check_age(
    27            self.age, self.passed_test
    28        )
    29
    30    def can_vote(self):
    

    the tests are still green.

  • I remove the commented lines from the can_get_license method

    20    def can_get_license(self):
    21        return self.check_age(
    22            self.age, self.passed_test
    23        )
    24
    25    def can_vote(self):
    
  • I add a git commit message in the other terminal

    git commit -am \
    'extract check_age method'
    

close the project

  • I close test_person.py and person.py

  • I click in the terminal where the tests are running

  • I use q on the keyboard to leave the tests. The terminal goes back to the command line.

  • I change directory to the parent of person

    cd ..
    

    the terminal shows

    ...\pumping_python
    

    I am back in the pumping_python directory.


review

I can use if statements to write a program that makes decisions based on conditions.

My tests have problems:


code from the chapter

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


what is next?

Would you like to test handling Exceptions in tests? Would you like to test booleans (there are only two)?


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.