how to make a person with Exceptions


I had a problem when I made a person with conditions, I skipped test_when_person_is_older_than_120, test_when_year_of_birth_is_the_future and test_when_year_of_birth_is_not_an_integer because they are always in a RED state since they cause Exceptions.

Python has a way that allows programs to make a choice when they encounter an Exception and continue running without stopping. It is the try statement.

I want to use the try statement to handle making sure the program raises an Exception

  • if the age is older than 120

  • if the year_of_birth is not an integer


preview

I have these tests by the end of the chapter

person/tests/test_person.py
222    def test_when_person_is_older_than_120(self):
223        try:
224            src.person.Person(
225                first_name='first_name',
226                last_name='last_name',
227                sex='M',
228                year_of_birth=self.this_year-121
229            )
230        except ValueError:
231            pass
person/tests/test_person.py
233    def test_when_year_of_birth_is_the_future(self):
234        try:
235            src.person.Person(
236                first_name='first_name',
237                last_name='last_name',
238                sex='F',
239                year_of_birth=self.this_year+1,
240            )
241        except ValueError:
242            pass
person/tests/test_person.py
244    def test_when_year_of_birth_is_not_an_integer(self):
245        try:
246            src.person.Person(
247                first_name='first_name',
248                last_name='last_name',
249                sex='M',
250                year_of_birth=None,
251            )
252        except TypeError:
253            pass
person/tests/test_person.py
255        try:
256            src.person.Person(
257                first_name='first_name',
258                last_name='last_name',
259                sex='F',
260                year_of_birth=2026.0,
261            )
262        except TypeError:
263            pass
person/tests/test_person.py
265        try:
266            src.person.Person(
267                first_name='first_name',
268                last_name='last_name',
269                sex='M',
270                year_of_birth='2026',
271            )
272        except TypeError:
273            pass
person/tests/test_person.py
275        try:
276            src.person.Person(
277                first_name='first_name',
278                last_name='last_name',
279                sex='F',
280                year_of_birth=(2026,),
281            )
282        except TypeError:
283            pass

open the project

  • I open a terminal

  • I change directory to the project

    cd person
    
  • I use pytest-watcher to run the tests automatically

    uv run pytest-watcher . --now --patterns "**/*.py"
    

    the terminal shows

    tests/test_person.py .......sss                 [100%]
    
    ============ 7 passed, 3 skipped in T.UVs ============
    

add exception handler to test_when_person_is_older_than_120

RED: make it fail


  • I open test_person.py from the tests folder

  • I comment out the unittest.skip decorator from test_when_person_is_older_than_120 in tests/test_person.py

    222    # @unittest.skip('fails because age > 120')
    223    def test_when_person_is_older_than_120(self):
    224        src.person.Person(
    225            first_name='first_name',
    226            last_name='last_name',
    227            sex='M',
    228            year_of_birth=self.this_year-121
    229        )
    230
    231    @unittest.skip('fails because age < 0')
    232    def test_when_year_of_birth_is_the_future(self):
    

    the terminal is my friend, and shows AssertionError

    E       AssertionError
    
  • I open __init__.py from the person folder in the src folder

  • I change assert age <= 120 to an if statement that raises an Exception if the age is greater than 120 in src/person/__init__.py

    37def calculate_age(year_of_birth):
    38    assert isinstance(year_of_birth, int)
    39    age = (
    40        datetime.date.today().year
    41      - year_of_birth
    42    )
    43    assert age >= 0
    44    # assert age <= 120
    45    if age > 120:
    46        raise Exception
    47    return age
    

    the terminal is my friend, and shows Exception

    E           Exception
    
  • I put the failing statement in an if statement in test_when_person_is_older_than_120 in tests/test_person.py

    222    # @unittest.skip('fails because age > 120')
    223    def test_when_person_is_older_than_120(self):
    224        if(
    225            src.person.Person(
    226                first_name='first_name',
    227                last_name='last_name',
    228                sex='M',
    229                year_of_birth=self.this_year-121
    230            )
    231        ) == Exception:
    232            pass
    233
    234    @unittest.skip('fails because age < 0')
    235    def test_when_year_of_birth_is_the_future(self):
    

    the terminal still shows Exception, because an Exception breaks execution of a program, nothing past the line that caused it will run. In this case, the Exception happens when the instance of the Person object is made so the condition never gets checked.

  • I change the if statement to a try statement

    222    # @unittest.skip('fails because age > 120')
    223    def test_when_person_is_older_than_120(self):
    224        try:
    225            src.person.Person(
    226                first_name='first_name',
    227                last_name='last_name',
    228                sex='M',
    229                year_of_birth=self.this_year-121
    230            )
    231        except:
    232            pass
    

    the test passes because the try statement is like an if statement for Exceptions.

    It tells the program what to do if an Exception is raised. A simple way to think of it is

    • try something

    • except - if something raises an Exception do something else

    pass is a special keyword that allows the try statement to follow Python language rules (the except block must have a body), it does not do anything.

  • I remove the commented line from test_when_person_is_older_than_120

    219        self.assertEqual(underage.can_vote(), False)
    220        self.assertEqual(underage.can_get_license(), False)
    221
    222    def test_when_person_is_older_than_120(self):
    223        try:
    224            src.person.Person(
    225                first_name='first_name',
    226                last_name='last_name',
    227                sex='M',
    228                year_of_birth=self.this_year-121
    229            )
    230        except:
    231            pass
    232
    233    @unittest.skip('fails because age < 0')
    234    def test_when_year_of_birth_is_the_future(self):
    
  • I open another terminal and change directories to person

    cd person
    
  • add a git commit message

    git commit -am \
    'add exception handler to test_when_person_is_older_than_120'
    

add exception handler to test_when_year_of_birth_is_the_future

RED: make it fail


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

  • I comment out the unittest.skip decorator from test_when_year_of_birth_is_the_future in tests/test_person.py

    233    # @unittest.skip('fails because age < 0')
    234    def test_when_year_of_birth_is_the_future(self):
    235        src.person.Person(
    236            first_name='first_name',
    237            last_name='last_name',
    238            sex='F',
    239            year_of_birth=self.this_year+1,
    240        )
    241
    242    @unittest.skip(
    243        'fails because year_of_birth is not an integer'
    244    )
    245    def test_when_year_of_birth_is_not_an_integer(self):
    

    the terminal is my friend, and shows AssertionError

    E       AssertionError
    

GREEN: make it pass


  • I change assert age >= 0 to an if statement that raises an Exception when year_of_birth is in the future, in the calculate_age function in src/person/__init__.py

    37def calculate_age(year_of_birth):
    38    assert isinstance(year_of_birth, int)
    39    age = (
    40        datetime.date.today().year
    41      - year_of_birth
    42    )
    43    # assert age >= 0
    44    if age < 0:
    45        raise Exception
    46    # assert age <= 120
    47    if age > 120:
    48        raise Exception
    49    return age
    

    the terminal is my friend, and shows Exception

    E           Exception
    
  • I put the failing statement in an if statement in test_when_year_of_birth_is_the_future in tests/test_person.py

    233    # @unittest.skip('fails because age < 0')
    234    def test_when_year_of_birth_is_the_future(self):
    235        if (
    236            src.person.Person(
    237                first_name='first_name',
    238                last_name='last_name',
    239                sex='F',
    240                year_of_birth=self.this_year+1,
    241            )
    242        ) == Exception:
    243            pass
    244
    245    @unittest.skip(
    246        'fails because year_of_birth is not an integer'
    247    )
    248    def test_when_year_of_birth_is_not_an_integer(self):
    

    the terminal still shows Exception, because an Exception breaks execution of a program, nothing past the line that caused it will run.

  • I change the if statement to a try statement

    233    # @unittest.skip('fails because age < 0')
    234    def test_when_year_of_birth_is_the_future(self):
    235        try:
    236            src.person.Person(
    237                first_name='first_name',
    238                last_name='last_name',
    239                sex='F',
    240                year_of_birth=self.this_year+1,
    241            )
    242        except:
    243            pass
    

    the test passes because try statement is like an if statement for Exceptions. It tells the program what to do if an Exception is raised


REFACTOR: make it better


  • I remove the commented line from test_when_year_of_birth_is_the_future

    222    def test_when_person_is_older_than_120(self):
    223        try:
    224            src.person.Person(
    225                first_name='first_name',
    226                last_name='last_name',
    227                sex='M',
    228                year_of_birth=self.this_year-121
    229            )
    230        except:
    231            pass
    232
    233    def test_when_year_of_birth_is_the_future(self):
    234        try:
    235            src.person.Person(
    236                first_name='first_name',
    237                last_name='last_name',
    238                sex='F',
    239                year_of_birth=self.this_year+1,
    240            )
    241        except:
    242            pass
    243
    244    @unittest.skip(
    245        'fails because year_of_birth is not an integer'
    246    )
    247    def test_when_year_of_birth_is_not_an_integer(self):
    
  • I put the two new if statements in the calculate_age function together as one because they return the same thing, in src/person/__init__.py

    37def calculate_age(year_of_birth):
    38    assert isinstance(year_of_birth, int)
    39    age = (
    40        datetime.date.today().year
    41      - year_of_birth
    42    )
    43    # assert age >= 0
    44    # if age < 0:
    45    #     raise Exception
    46    # assert age <= 120
    47    # if age > 120:
    48    if age < 0 or age > 120:
    49        raise Exception
    50    return age
    

    the test is still green.

  • I remove the commented lines from the calculate_age function

    37def calculate_age(year_of_birth):
    38    assert isinstance(year_of_birth, int)
    39    age = (
    40        datetime.date.today().year
    41      - year_of_birth
    42    )
    43
    44    if age < 0 or age > 120:
    45        raise Exception
    46    return age
    
  • I add a git commit message in the other terminal

    git commit -am \
    'add exception handler to test_when_year_of_birth_is_the_future'
    

add exception handler to test_when_year_of_birth_is_not_an_integer

RED: make it fail


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

  • I comment out the unittest.skip decorator from test_when_year_of_birth_is_not_an_integer and remove the comment from year_of_birth=None to test when year_of_birth is None, in tests/test_person.py

    244    # @unittest.skip(
    245    #     'fails because year_of_birth is not an integer'
    246    # )
    247    def test_when_year_of_birth_is_not_an_integer(self):
    248        src.person.Person(
    249            first_name='first_name',
    250            last_name='last_name',
    251            sex='M',
    252            year_of_birth=None,
    253            # year_of_birth=2026.0,   # fails
    254            # year_of_birth='2026',   # fails
    255            # year_of_birth=(2026,),  # fails
    256        )
    257
    258    def test_dir_person_object(self):
    

    the terminal is my friend, and shows AssertionError

    E       AssertionError
    

GREEN: make it pass


  • I change assert isinstance(year_of_birth, int) to an if statement that raises an Exception when year_of_birth is not an integer in the calculate_age function in src/person/__init__.py

    37def calculate_age(year_of_birth):
    38    # assert isinstance(year_of_birth, int)
    39    if not isinstance(year_of_birth, int):
    40        raise Exception
    41
    42    age = (
    43        datetime.date.today().year
    44      - year_of_birth
    45    )
    46
    47    if age < 0 or age > 120:
    48        raise Exception
    49    return age
    

    the terminal is my friend, and shows Exception

    E           Exception
    
  • I move the failing statement to a try statement for when year_of_birth is None in test_when_year_of_birth_is_not_an_integer in tests/test_person.py

    244    # @unittest.skip(
    245    #     'fails because year_of_birth is not an integer'
    246    # )
    247    def test_when_year_of_birth_is_not_an_integer(self):
    248        try:
    249            src.person.Person(
    250                first_name='first_name',
    251                last_name='last_name',
    252                sex='M',
    253                year_of_birth=None,
    254            )
    255        except:
    256            pass
    257        # year_of_birth=2026.0,   # fails
    258        # year_of_birth='2026',   # fails
    259        # year_of_birth=(2026,),  # fails
    260
    261    def test_dir_person_object(self):
    

    the test passes.


REFACTOR: make it better


  • I make a person with a float as the value for year_of_birth

    255    except:
    256        pass
    257
    258    src.person.Person(
    259        first_name='first_name',
    260        last_name='last_name',
    261        sex='F',
    262        year_of_birth=2026.0,
    263    )
    264
    265    # year_of_birth='2026',   # fails
    266    # year_of_birth=(2026,),  # fails
    

    the terminal is my friend, and shows Exception

  • I move the failing statement for when the year_of_birth is a float to a try statement

    255        except:
    256            pass
    257
    258        try:
    259            src.person.Person(
    260                first_name='first_name',
    261                last_name='last_name',
    262                sex='F',
    263                year_of_birth=2026.0,
    264            )
    265        except:
    266            pass
    267
    268        # year_of_birth='2026',  # fails
    

    the test passes.

  • I make a person with a string as the value for year_of_birth

    265        except:
    266            pass
    267
    268        src.person.Person(
    269            first_name='first_name',
    270            last_name='last_name',
    271            sex='M',
    272            year_of_birth='2026',
    273        )
    274
    275        # year_of_birth=(2026,),  # fails
    

    the terminal is my friend, and shows Exception

  • I move the statement for when the year_of_birth is a string to a try statement

    265        except:
    266            pass
    267
    268        try:
    269            src.person.Person(
    270                first_name='first_name',
    271                last_name='last_name',
    272                sex='M',
    273                year_of_birth='2026',
    274            )
    275        except:
    276            pass
    277
    278        # year_of_birth=(2026,),  # fails
    

    the test passes.

  • I make a person with a tuple as the value for year_of_birth

    275        except:
    276            pass
    277
    278        src.person.Person(
    279            first_name='first_name',
    280            last_name='last_name',
    281            sex='F',
    282            year_of_birth=(2026,),
    283        )
    284
    285    def test_dir_person_object(self):
    

    the terminal is my friend, and shows Exception

  • I move the statement for when the year_of_birth is a tuple to a try statement

    275        except:
    276            pass
    277
    278        try:
    279            src.person.Person(
    280                first_name='first_name',
    281                last_name='last_name',
    282                sex='F',
    283                year_of_birth=(2026,),
    284            )
    285        except:
    286            pass
    287
    288    def test_dir_person_object(self):
    

    the test passes.

  • I remove the commented line from the calculate_age function in src/person/__init__.py

    37def calculate_age(year_of_birth):
    38    if not isinstance(year_of_birth, int):
    39        raise Exception
    40
    41    age = (
    42        datetime.date.today().year
    43      - year_of_birth
    44    )
    45
    46    if age < 0 or age > 120:
    47        raise Exception
    48    return age
    
  • I add a git commit message in the other terminal

    git commit -am \
    'add exception handler to test_when_year_of_birth_is_not_an_integer'
    

raise TypeError when year_of_birth is not an integer

The problem with using except: is that it catches all Exceptions which means it does not tell anyone that reads the code what the actual Exception is.

try:
    something
except:
    something else

is the same as

try:
    something
except Exception:
    something else

because Exception is the mother of all the Exceptions covered so far, they inherit from it.

From the Zen of Python: Explicit is better than implicit. I want to make things clearer.

RED: make it fail



GREEN: make it pass


I change the raise statement in the calculate_age function for when the year_of_birth is not an integer to make it clearer, in src/person/__init__.py

37def calculate_age(year_of_birth):
38    if not isinstance(year_of_birth, int):
39        raise TypeError
40        raise Exception
41
42    age = (
43        datetime.date.today().year
44      - year_of_birth
45    )

the test passes because the try statement now only catches/handles TypeError, all other Exceptions will still break execution of this program.


REFACTOR: make it better


  • I change the except clause in test_when_year_of_birth_is_not_an_integer for when year_of_birth is a string to show that it catches Exception

    268        try:
    269            src.person.Person(
    270                first_name='first_name',
    271                last_name='last_name',
    272                sex='M',
    273                year_of_birth='2026',
    274            )
    275        except Exception:
    276            pass
    

    the test is still green.

  • I change the except clause to make it only catch AssertionError

    268        try:
    269            src.person.Person(
    270                first_name='first_name',
    271                last_name='last_name',
    272                sex='M',
    273                year_of_birth='2026',
    274            )
    275        except AssertionError:
    276            pass
    

    the terminal is my friend, and shows TypeError

    E           TypeError
    

    because TypeError is not AssertionError or a child of AssertionError.

  • I change the except clause in test_when_year_of_birth_is_not_an_integer for when year_of_birth is a string to only catch TypeError

    268        try:
    269            src.person.Person(
    270                first_name='first_name',
    271                last_name='last_name',
    272                sex='M',
    273                year_of_birth='2026',
    274            )
    275        except TypeError:
    276            pass
    

    the test passes.

  • I change the except clause in test_when_year_of_birth_is_not_an_integer for when year_of_birth is a float to show that it catches Exception

    258        try:
    259            src.person.Person(
    260                first_name='first_name',
    261                last_name='last_name',
    262                sex='F',
    263                year_of_birth=2026.0,
    264            )
    265        except Exception:
    266            pass
    

    the test is still green.

  • I change the except clause to make it only catch NameError

    258        try:
    259            src.person.Person(
    260                first_name='first_name',
    261                last_name='last_name',
    262                sex='F',
    263                year_of_birth=2026.0,
    264            )
    265        except NameError:
    266            pass
    

    the terminal is my friend, and shows TypeError

    E           TypeError
    

    because TypeError is not NameError or a child of NameError.

  • I change the except clause in test_when_year_of_birth_is_not_an_integer for when the year_of_birth is a float to make it only catch TypeError

    258        try:
    259            src.person.Person(
    260                first_name='first_name',
    261                last_name='last_name',
    262                sex='F',
    263                year_of_birth=2026.0,
    264            )
    265        except TypeError:
    266            pass
    

    the test passes.

  • I change the except clause in test_when_year_of_birth_is_not_an_integer for when the year_of_birth is None to show that it catches Exception

    244    # @unittest.skip(
    245    #     'fails because year_of_birth is not an integer'
    246    # )
    247    def test_when_year_of_birth_is_not_an_integer(self):
    248        try:
    249            src.person.Person(
    250                first_name='first_name',
    251                last_name='last_name',
    252                sex='M',
    253                year_of_birth=None,
    254            )
    255        except Exception:
    256            pass
    

    the test is still green.

  • I change the except clause to make it only catch ValueError

    247    def test_when_year_of_birth_is_not_an_integer(self):
    248        try:
    249            src.person.Person(
    250                first_name='first_name',
    251                last_name='last_name',
    252                sex='M',
    253                year_of_birth=None,
    254            )
    255        except ValueError:
    256            pass
    

    the terminal is my friend, and shows TypeError

    E           TypeError
    

    because TypeError is not ValueError or a child of ValueError.

  • I change the except clause in test_when_year_of_birth_is_not_an_integer for when the year_of_birth is None to make it catch/handle TypeError

    244    # @unittest.skip(
    245    #     'fails because year_of_birth is not an integer'
    246    # )
    247    def test_when_year_of_birth_is_not_an_integer(self):
    248        try:
    249            src.person.Person(
    250                first_name='first_name',
    251                last_name='last_name',
    252                sex='M',
    253                year_of_birth=None,
    254            )
    255        except TypeError:
    256            pass
    

    the test passes.

  • I remove the commented lines from test_when_year_of_birth_is_not_an_integer

    233    def test_when_year_of_birth_is_the_future(self):
    234        try:
    235            src.person.Person(
    236                first_name='first_name',
    237                last_name='last_name',
    238                sex='F',
    239                year_of_birth=self.this_year+1,
    240            )
    241        except:
    242            pass
    243
    244    def test_when_year_of_birth_is_not_an_integer(self):
    245        try:
    246            src.person.Person(
    247                first_name='first_name',
    248                last_name='last_name',
    249                sex='M',
    250                year_of_birth=None,
    251            )
    252        except TypeError:
    253            pass
    
  • I add a git commit message in the other terminal

    git commit -am \
    'raise TypeError when year_of_birth is not an integer'
    

raise ValueError when age is greater than 120 or less than 0

RED: make it fail



GREEN: make it pass


I change the raise statement in the calculate_age function for when the age is greater than 120 or less than 0 to make it clearer, in src/person/__init__.py

37def calculate_age(year_of_birth):
38    if not isinstance(year_of_birth, int):
39        raise TypeError
40        raise Exception
41
42    age = (
43        datetime.date.today().year
44      - year_of_birth
45    )
46
47    if age < 0 or age > 120:
48        raise ValueError
49        raise Exception
50    return age

the test passes.


REFACTOR: make it better


  • I remove raise Exception from the calculate_age function

    37def calculate_age(year_of_birth):
    38    if not isinstance(year_of_birth, int):
    39        raise TypeError
    40
    41    age = (
    42        datetime.date.today().year
    43      - year_of_birth
    44    )
    45
    46    if age < 0 or age > 120:
    47        raise ValueError
    48    return age
    
  • I change the except clause in test_when_person_is_older_than_120 to show that it catches Exception

    222    def test_when_person_is_older_than_120(self):
    223        try:
    224            src.person.Person(
    225                first_name='first_name',
    226                last_name='last_name',
    227                sex='M',
    228                year_of_birth=self.this_year-121
    229            )
    230        except Exception:
    231            pass
    

    the test is still green.

  • I change the except clause to make it only catch TypeError

    222    def test_when_person_is_older_than_120(self):
    223        try:
    224            src.person.Person(
    225                first_name='first_name',
    226                last_name='last_name',
    227                sex='M',
    228                year_of_birth=self.this_year-121
    229            )
    230        except TypeError:
    231            pass
    

    the terminal is my friend, and shows ValueError

    E           ValueError
    
  • I change the except clause to make it only catch ValueError

    222    def test_when_person_is_older_than_120(self):
    223        try:
    224            src.person.Person(
    225                first_name='first_name',
    226                last_name='last_name',
    227                sex='M',
    228                year_of_birth=self.this_year-121
    229            )
    230        except ValueError:
    231            pass
    

    the test is green again.

  • I add a git commit message in the other terminal

    git commit -am \
    'raise ValueError when age > 120 or age < 0'
    

close the project

  • I close tests/test_person.py and src/person/__init__.py

  • 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 ..
    

review

My tests still 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?


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.