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
120if the
year_of_birthis not an integer
preview
I have these tests by the end of the chapter
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
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
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
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
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
open the project
I open a terminal
I change directory to the project
cd personI 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.pyfrom thetestsfolderI comment out the unittest.skip decorator from test_when_person_is_older_than_120 in
tests/test_person.py222 # @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 AssertionErrorI change
assert age <= 120to an if statement that raises an Exception if the age is greater than120insrc/person/__init__.py37def 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 agethe terminal is my friend, and shows Exception
E ExceptionI put the failing statement in an if statement in test_when_person_is_older_than_120 in
tests/test_person.py222 # @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 passthe 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
trysomethingexcept- 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
personcd personadd 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.py233 # @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 >= 0to an if statement that raises an Exception whenyear_of_birthis in the future, in the calculate_age function insrc/person/__init__.py37def 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 agethe terminal is my friend, and shows Exception
E ExceptionI put the failing statement in an if statement in test_when_year_of_birth_is_the_future in
tests/test_person.py233 # @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 passthe 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__.py37def 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 agethe 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 ageI 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=Noneto test whenyear_of_birthis None, intests/test_person.py244 # @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 whenyear_of_birthis not an integer in the calculate_age function insrc/person/__init__.py37def 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 agethe terminal is my friend, and shows Exception
E ExceptionI move the failing statement to a try statement for when
year_of_birthis None in test_when_year_of_birth_is_not_an_integer intests/test_person.py244 # @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_birth255 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,), # failsI move the failing statement for when the
year_of_birthis a float to a try statement255 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', # failsthe test passes.
I make a person with a string as the value for
year_of_birth265 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,), # failsI move the statement for when the
year_of_birthis a string to a try statement265 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,), # failsthe test passes.
I make a person with a tuple as the value for
year_of_birth275 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):I move the statement for when the
year_of_birthis a tuple to a try statement275 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__.py37def 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 ageI 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
I go back to the terminal where the tests are running
I change the except clause in test_when_year_of_birth_is_not_an_integer for when the
year_of_birthis a tuple to show that it catches Exception278 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 Exception: 286 pass 287 288 def test_dir_person_object(self):the test is still green.
I change the except clause to make it only catch TypeError
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 TypeError: 286 pass 287 288 def test_dir_person_object(self):the terminal is my friend, and shows Exception
E Exceptionbecause Exception is not TypeError even though TypeError is an Exception. I cannot use a child Exception to catch its parent Exception.
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_birthis a string to show that it catches Exception268 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 passthe 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 passthe terminal is my friend, and shows TypeError
E TypeErrorbecause 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_birthis a string to only catch TypeError268 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 passthe test passes.
I change the except clause in test_when_year_of_birth_is_not_an_integer for when
year_of_birthis a float to show that it catches Exception258 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 passthe 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 passthe terminal is my friend, and shows TypeError
E TypeErrorI change the except clause in test_when_year_of_birth_is_not_an_integer for when the
year_of_birthis a float to make it only catch TypeError258 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 passthe test passes.
I change the except clause in test_when_year_of_birth_is_not_an_integer for when the
year_of_birthis None to show that it catches Exception244 # @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 passthe 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 passthe terminal is my friend, and shows TypeError
E TypeErrorbecause 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_birthis None to make it catch/handle TypeError244 # @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 passthe 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 passI 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
I go back to the terminal where the tests are running
I change the except clause in test_when_year_of_birth_is_the_future to catch ValueError
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 243 244 def test_when_year_of_birth_is_not_an_integer(self):the terminal is my friend, and shows Exception
E Exceptionbecause Exception is not ValueError even though ValueError is an Exception. I cannot use a child Exception to catch its parent Exception.
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 Exceptionfrom the calculate_age function37def 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 ageI 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 passthe 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 passthe terminal is my friend, and shows ValueError
E ValueErrorI 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 passthe 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.pyandsrc/person/__init__.pyI use q on the keyboard to leave the tests. The terminal goes back to the command line.
I change directory to the parent of
personcd ..
review
Exceptions break execution of a program, nothing past the line that causes an Exception will run.
I can use the try statement to make sure a program can make a decision when an Exception is raised.
I can use the try statement in a test to show that a program raises an Exception when certain conditions are met.
I can use the raise statement to make an Exception happen to stop a program from running past a certain point.
My tests still have problems:
The attribute tests - test_dir_person_object and test_dir_person_instance catch changes to the attributes and methods of the Person object and they are a problem to maintain. There has to be a better way.
test_joe, test_jane, test_john and test_mary all still have the same three tests. There has to be a better way.
test_when_year_of_birth_is_not_an_integer has four tests that are basically the same, the only thing that changes are the values for the
year_of_birthparameter. There has to be a better way.The exception handlers also have a problem
code from the chapter
what is next?
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.