how to make a person with conditions


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

  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.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 it helps document what is in the class and catches things immediately it changes.

    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 a class 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 a class 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 older than 18?


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 class attribute

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


RED: make it fail


I add a class 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 class 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 __init__ method 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
    

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