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. Which means I want something in the person project to make decisions based on conditions

  • If a person is younger than 18, the person cannot get a license.

  • If a person is younger than 18, the person cannot vote.

  • If a person is 18 or older and passes a test, the person can get a license.

  • If a person is 18 or older and the person is a citizen, the person can vote.


preview

I have these tests by the end of the chapter

person/tests/test_person.py
65    def test_joe(self):
66        first_name = 'joe'
67        last_name = 'blow'
68        sex = 'M'
69        year_of_birth = 1996
70
71        self.assert_person_factory_works(
72            first_name=first_name,
73            last_name=last_name,
74            sex=sex,
75            year_of_birth=year_of_birth
76        )
77
78        self.assert_say_hello_works(
79            first_name=first_name,
80            last_name=last_name,
81            year_of_birth=year_of_birth,
82        )
83
84        self.assert_person_can_say_hello(
85            first_name=first_name,
86            last_name=last_name,
87            sex=sex,
88            year_of_birth=year_of_birth
89        )
90
91        joe = src.person.Person(
92            first_name=first_name,
93            last_name=last_name,
94            sex=sex,
95            year_of_birth=year_of_birth,
96        )
97        self.assertEqual(joe.can_vote(), True)
98        self.assertEqual(joe.can_get_license(), False)
person/tests/test_person.py
100    def test_jane(self):
101        first_name = 'jane'
102        last_name = 'doe'
103        sex = 'F'
104        year_of_birth = 1991
105
106        self.assert_person_factory_works(
107            first_name=first_name,
108            last_name=last_name,
109            sex=sex,
110            year_of_birth=year_of_birth,
111        )
112
113        self.assert_say_hello_works(
114            first_name=first_name,
115            last_name=last_name,
116            year_of_birth=year_of_birth,
117        )
118
119        self.assert_person_can_say_hello(
120            first_name=first_name,
121            last_name=last_name,
122            sex=sex,
123            year_of_birth=year_of_birth,
124        )
125
126        jane = src.person.Person(
127            first_name=first_name,
128            last_name=last_name,
129            sex=sex,
130            year_of_birth=year_of_birth,
131            passed_test=True,
132        )
133        self.assertEqual(jane.can_vote(), True)
134        self.assertEqual(jane.can_get_license(), True)
person/tests/test_person.py
136    def test_john(self):
137        first_name = 'john'
138        last_name = 'smith'
139        sex = 'M'
140        year_of_birth = 1980
141
142        self.assert_person_factory_works(
143            first_name=first_name,
144            last_name=last_name,
145            sex=sex,
146            year_of_birth=year_of_birth,
147        )
148
149        self.assert_say_hello_works(
150            first_name=first_name,
151            last_name=last_name,
152            year_of_birth=year_of_birth,
153        )
154
155        self.assert_person_can_say_hello(
156            first_name=first_name,
157            last_name=last_name,
158            sex=sex,
159            year_of_birth=year_of_birth,
160        )
161
162        john = src.person.Person(
163            first_name=first_name,
164            last_name=last_name,
165            sex=sex,
166            year_of_birth=year_of_birth,
167            is_citizen=False,
168            passed_test=False,
169        )
170        self.assertEqual(john.can_vote(), False)
171        self.assertEqual(john.can_get_license(), False)
person/tests/test_person.py
173    def test_mary(self):
174        first_name = 'mary'
175        last_name = 'public'
176        sex = 'F'
177        year_of_birth = 2000
178
179        self.assert_person_factory_works(
180            first_name=first_name,
181            last_name=last_name,
182            sex=sex,
183            year_of_birth=year_of_birth,
184        )
185
186        self.assert_say_hello_works(
187            first_name=first_name,
188            last_name=last_name,
189            year_of_birth=year_of_birth,
190        )
191
192        self.assert_person_can_say_hello(
193            first_name=first_name,
194            last_name=last_name,
195            sex=sex,
196            year_of_birth=year_of_birth,
197        )
198
199        mary = src.person.Person(
200            first_name=first_name,
201            last_name=last_name,
202            sex=sex,
203            year_of_birth=year_of_birth,
204            is_citizen=False,
205            passed_test=True,
206        )
207        self.assertEqual(mary.can_vote(), False)
208        self.assertEqual(mary.can_get_license(), True)
person/tests/test_person.py
210    def test_underage_citizen(self):
211        underage = src.person.Person(
212            first_name='first_name',
213            last_name='last_name',
214            sex='M',
215            year_of_birth=self.this_year-17,
216            is_citizen=True,
217            passed_test=True,
218        )
219        self.assertEqual(underage.can_vote(), False)
220        self.assertEqual(underage.can_get_license(), False)
person/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        )
person/tests/test_person.py
231    @unittest.skip('fails because age < 0')
232    def test_when_year_of_birth_is_the_future(self):
233        src.person.Person(
234            first_name='first_name',
235            last_name='last_name',
236            sex='F',
237            year_of_birth=self.this_year+1,
238        )
person/tests/test_person.py
240    @unittest.skip(
241        'fails because year_of_birth is not an integer'
242    )
243    def test_when_year_of_birth_is_not_an_integer(self):
244        src.person.Person(
245            first_name='first_name',
246            last_name='last_name',
247            sex='M',
248            # year_of_birth=None,     # fails
249            # year_of_birth=2026.0,   # fails
250            # year_of_birth='2026',   # fails
251            # year_of_birth=(2026,),  # fails
252        )
person/tests/test_person.py
254    def test_dir_person_class(self):
255        self.assertEqual(
256            dir(src.person.Person),
257            [
258                '__class__', '__delattr__', '__dict__',
259                '__dir__', '__doc__', '__eq__',
260                '__firstlineno__', '__format__', '__ge__',
261                '__getattribute__', '__getstate__', '__gt__',
262                '__hash__', '__init__', '__init_subclass__',
263                '__le__', '__lt__', '__module__', '__ne__',
264                '__new__', '__reduce__', '__reduce_ex__',
265                '__repr__', '__setattr__', '__sizeof__',
266                '__static_attributes__', '__str__',
267                '__subclasshook__', '__weakref__',
268                'can_get_license', 'can_vote', 'check_age',
269                'say_hello',
270            ]
271        )
person/tests/test_person.py
273    def test_dir_person_instance(self):
274        self.assertEqual(
275            dir(
276                src.person.Person(
277                    first_name='first_name',
278                    last_name='last_name',
279                    sex='M',
280                    year_of_birth=2026,
281                )
282            ),
283            [
284                '__class__', '__delattr__', '__dict__',
285                '__dir__', '__doc__', '__eq__',
286                '__firstlineno__', '__format__', '__ge__',
287                '__getattribute__', '__getstate__', '__gt__',
288                '__hash__', '__init__', '__init_subclass__',
289                '__le__', '__lt__', '__module__', '__ne__',
290                '__new__', '__reduce__', '__reduce_ex__',
291                '__repr__', '__setattr__', '__sizeof__',
292                '__static_attributes__', '__str__',
293                '__subclasshook__', '__weakref__',
294                'age', 'can_get_license', 'can_vote',
295                'check_age', 'first_name', 'is_citizen',
296                'last_name', 'passed_test', 'say_hello',
297                'sex', 'year_of_birth',
298            ]
299        )
300
301
302# Exceptions seen
303# AssertionError
304# NameError
305# TypeError
306# AttributeError
307# 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%]
    
    =================== 9 passed in P.QRs ====================
    

add can_vote method

RED: make it fail


I add a call to can_vote from test_joe

65  def test_joe(self):
66      first_name = 'joe'
67      last_name = 'blow'
68      ...
84      self.assert_person_can_say_hello(
85          first_name=first_name,
86          last_name=last_name,
87          sex=sex,
88          year_of_birth=year_of_birth
89      )
90
91      joe = src.person.Person(
92          first_name=first_name,
93          last_name=last_name,
94          sex=sex,
95          year_of_birth=year_of_birth,
96      )
97      self.assertEqual(joe.can_vote(), True)
98
99  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 __init__.py from the person folder in the src folder

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

     4class 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):
    

    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

     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    @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 in tests/test_person.py

    210    def test_dir_person_class(self):
    211        self.assertEqual(
    212            dir(src.person.Person),
    213            [
    214                '__class__', '__delattr__', '__dict__',
    215                ...
    
    222                '__static_attributes__', '__str__',
    223                '__subclasshook__', '__weakref__',
    224                'can_vote', 'say_hello',
    225            ]
    226        )
    227
    228    def test_dir_person_instance(self):
    
  • I add can_vote to test_dir_person_instance

    228    def test_dir_person_instance(self):
    229        self.assertEqual(
    230            dir(
    231                src.person.Person(
    232                    first_name='first_name',
    233                    last_name='last_name',
    234                    sex='M',
    235                    year_of_birth=2026,
    236                )
    237            ),
    238            [
    239                '__class__', '__delattr__', '__dict__',
    240                ...
    
    248                '__subclasshook__', '__weakref__',
    249                'can_vote', 'first_name', 'last_name',
    250                'say_hello', 'sex', 'year_of_birth',
    251            ]
    252        )
    253
    254
    255# Exceptions seen
    

    the test passes.

    test_dir_person_class and test_dir_person_instance are good because they help document what is in the object and catch its changes immediately.

    test_dir_person_class and test_dir_person_instance are a problem because object attributes can change from one Python version to another, 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_mary

    151    def test_mary(self):
    152        first_name = 'mary'
    153        last_name = 'public'
    154        ...
    
    170        self.assert_person_can_say_hello(
    171            first_name=first_name,
    172            last_name=last_name,
    173            sex=sex,
    174            year_of_birth=year_of_birth,
    175        )
    176
    177        mary = src.person.Person(
    178            first_name=first_name,
    179            last_name=last_name,
    180            sex=sex,
    181            year_of_birth=year_of_birth
    182        )
    183        self.assertEqual(mary.can_vote(), False)
    184
    185    def test_when_person_is_older_than_120(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 instance of the Person class for mary

    177        mary = src.person.Person(
    178            first_name=first_name,
    179            last_name=last_name,
    180            sex=sex,
    181            year_of_birth=year_of_birth,
    182            is_citizen=False,
    183        )
    184        self.assertEqual(mary.can_vote(), False)
    

    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 of the Person class only takes five inputs (self, first_name, last_name, sex and year_of_birth) and it got called it with is_citizen which is not one of those names.

  • I add is_citizen to the parentheses of the __init__ method, in src/person/__init__.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=True,
    10        ):
    

    the terminal goes back to the AssertionError.

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

     4class Person:
     5
     6    def __init__(
     7            self, first_name, last_name,
     8            sex, year_of_birth=None,
     9            is_citizen=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    @staticmethod
    18    def can_vote():
    

    still AssertionError.

  • I use the object attribute in the can_vote method

    17    @staticmethod
    18    def can_vote():
    19        return self.is_citizen
    20        return True
    

    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

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

    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 the expectation of the assertion in test_dir_person_instance in tests/test_person.py

    237    def test_dir_person_instance(self):
    238        self.assertEqual(
    239            dir(
    240                src.person.Person(
    241                    first_name='first_name',
    242                    last_name='last_name',
    243                    sex='M',
    244                    year_of_birth=2026,
    245                )
    246            ),
    247            [
    248                '__class__', '__delattr__', '__dict__',
    249                ...
    
    257                '__subclasshook__', '__weakref__',
    258                'can_vote', 'first_name', 'is_citizen',
    259                'last_name', 'say_hello', 'sex',
    260                'year_of_birth',
    261            ]
    262        )
    263
    264
    265# Exceptions seen
    

    the test passes.

  • I remove the commented lines and the second return statement from the can_vote method in src/person/__init__.py

     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):
    

REFACTOR: make it better


  • I add a call to can_vote from test_john, in tests/test_person.py

    125    def test_john(self):
    126        first_name = 'john'
    127        last_name = 'smith'
    128        ...
    
    144        self.assert_person_can_say_hello(
    145            first_name=first_name,
    146            last_name=last_name,
    147            sex=sex,
    148            year_of_birth=year_of_birth,
    149        )
    150
    151        john = src.person.Person(
    152            first_name=first_name,
    153            last_name=last_name,
    154            sex=sex,
    155            year_of_birth=year_of_birth,
    156        )
    157        self.assertEqual(john.can_vote(), False)
    158
    159    def test_mary(self):
    

    the terminal is my friend, and shows AssertionError

    AssertionError: True != False
    
  • I add is_citizen to the instance of the Person class for john

    151        john = src.person.Person(
    152            first_name=first_name,
    153            last_name=last_name,
    154            sex=sex,
    155            year_of_birth=year_of_birth,
    156            is_citizen=False,
    157        )
    158        self.assertEqual(john.can_vote(), False)
    

    the test passes.

  • I add a call to can_vote from test_jane

     99    def test_jane(self):
    100        first_name = 'jane'
    101        last_name = 'doe'
    102        ...
    
    118        self.assert_person_can_say_hello(
    119            first_name=first_name,
    120            last_name=last_name,
    121            sex=sex,
    122            year_of_birth=year_of_birth,
    123        )
    124
    125        jane = src.person.Person(
    126            first_name=first_name,
    127            last_name=last_name,
    128            sex=sex,
    129            year_of_birth=year_of_birth,
    130        )
    131        self.assertEqual(jane.can_vote(), False)
    132
    133    def test_john(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 and the default value for is_citizen is True.

  • I change the expectation of the assertion in test_jane

    125        jane = src.person.Person(
    126            first_name=first_name,
    127            last_name=last_name,
    128            sex=sex,
    129            year_of_birth=year_of_birth,
    130        )
    131        # self.assertEqual(jane.can_vote(), False)
    132        self.assertEqual(jane.can_vote(), True)
    133
    134    def test_john(self):
    

    the test passes.

  • I remove the commented line from test_jane

    125        jane = src.person.Person(
    126            first_name=first_name,
    127            last_name=last_name,
    128            sex=sex,
    129            year_of_birth=year_of_birth,
    130        )
    131        self.assertEqual(jane.can_vote(), True)
    

    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.

  • 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

    201        self.assertEqual(mary.can_vote(), False)
    202
    203    def test_underage_citizen(self):
    204        underage = src.person.Person(
    205            first_name='first_name',
    206            last_name='last_name',
    207            sex='M',
    208            year_of_birth=self.this_year-17,
    209            is_citizen=True,
    210        )
    211        self.assertEqual(underage.can_vote(), False)
    212
    213    def test_when_person_is_older_than_120(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 src/person/__init__.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 Python checks if age (which is the result of calculate_age(self.year_of_birth)) is less than 18, when if age < 18: runs

    • 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() -> False
        └── class Person:
            └── def can_vote(self):
                └── if age < 18:
                    └── return False
                    return self.is_citizen
      
      self.is_citizen = True
      age < 18
      
      person.can_vote() -> False
        └── class Person:
            └── def can_vote(self):
                └── if age < 18:
                    └── return False
                    return self.is_citizen
      
    • 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() -> False
        └── class Person:
            └── 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() -> True
        └── class Person:
            └── 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.

  • 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 go back to the terminal where the tests are running

  • I add a call to can_get_license from test_mary in tests/test_person.py

    194        mary = src.person.Person(
    195            first_name=first_name,
    196            last_name=last_name,
    197            sex=sex,
    198            year_of_birth=year_of_birth,
    199            is_citizen=False,
    200        )
    201        self.assertEqual(mary.can_vote(), False)
    202        self.assertEqual(mary.can_get_license(), True)
    203
    204    def test_underage_citizen(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 for can_get_license to the Person class in src/person/__init__.py

     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

    15        self.is_citizen = is_citizen
    16
    17    @staticmethod
    18    def can_get_license():
    19        return True
    

    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 tests/test_person.py

    247    def test_dir_person_class(self):
    248        self.assertEqual(
    249            dir(src.person.Person),
    250            [
    251                '__class__', '__delattr__', '__dict__',
    252                ...
    
    260                '__subclasshook__', '__weakref__',
    261                'can_get_license', 'can_vote', 'say_hello'
    262            ]
    263        )
    264
    265    def test_dir_person_instance(self):
    
  • I add can_get_license to test_dir_person_instance

    265    def test_dir_person_instance(self):
    266        self.assertEqual(
    267            dir(
    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            [
    276                '__class__', '__delattr__', '__dict__',
    277                ...
    
    285                '__subclasshook__', '__weakref__',
    286                'can_get_license', 'can_vote', 'first_name',
    287                'is_citizen', 'last_name', 'say_hello',
    288                'sex', 'year_of_birth',
    289            ]
    290        )
    291
    292
    293# 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 go back to the terminal where the tests are running

  • I add a call to can_get_license from test_john

    159        john = src.person.Person(
    160            first_name=first_name,
    161            last_name=last_name,
    162            sex=sex,
    163            year_of_birth=year_of_birth,
    164            is_citizen=False,
    165        )
    166        self.assertEqual(john.can_vote(), False)
    167        self.assertEqual(john.can_get_license(), False)
    168
    169    def test_mary(self):
    

    the terminal is my friend, and shows AssertionError

    AssertionError: True != False
    

GREEN: make it pass


  • I add passed_test to the instance of the Person class for john

    159        john = src.person.Person(
    160            first_name=first_name,
    161            last_name=last_name,
    162            sex=sex,
    163            year_of_birth=year_of_birth,
    164            is_citizen=False,
    165            passed_test=False,
    166        )
    167        self.assertEqual(john.can_vote(), False)
    168        self.assertEqual(john.can_get_license(), False)
    

    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 allows six inputs (self, first_name, last_name, sex, year_of_birth and is_citizen) and it got called with passed_test which is not one of those names.

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

     4class Person:
     5
     6    def __init__(
     7            self, first_name, last_name,
     8            sex, year_of_birth=None,
     9            is_citizen=True, passed_test,
    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 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, passed_test=False,
    10        ):
    

    the terminal goes back to the AssertionError.

  • I use self.passed_test in the can_get_license method

    17    @staticmethod
    18    def can_get_license():
    19        return self.passed_test
    20        return True
    

    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

    17    # @staticmethod
    18    # def can_get_license():
    19    def can_get_license(self):
    20        return self.passed_test
    21        return True
    

    the terminal is my friend, and shows AttributeError

    AttributeError: 'Person' object has no attribute 'passed_test'
    
  • I add an instance attribute for passed_test so I can use it in the can_get_license method

     6    def __init__(
     7            self, first_name, last_name,
     8            sex, year_of_birth=None,
     9            is_citizen=True, passed_test=False,
    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        self.passed_test = passed_test
    17
    18    @staticmethod
    19    def can_get_license():
    

    the terminal shows AssertionError for test_mary

    AssertionError: False != True
    
  • I add passed_test to the instance of the Person class for mary

    196        mary = src.person.Person(
    197            first_name=first_name,
    198            last_name=last_name,
    199            sex=sex,
    200            year_of_birth=year_of_birth,
    201            is_citizen=False,
    202            passed_test=True,
    203        )
    204        self.assertEqual(mary.can_vote(), False)
    205        self.assertEqual(mary.can_get_license(), True)
    

    the terminal shows AssertionError for test_dir_person_instance because I added a new attribute (passed_test).

  • I add passed_test to the expectation of the assertion in test_dir_person_instance in tests/test_person.py

    268    def test_dir_person_instance(self):
    269        self.assertEqual(
    270            dir(
    271                src.person.Person(
    272                    first_name='first_name',
    273                    last_name='last_name',
    274                    sex='M',
    275                    year_of_birth=2026,
    276                )
    277            ),
    278            [
    279                '__class__', '__delattr__', '__dict__',
    280                ...
    
    289                'can_get_license', 'can_vote', 'first_name',
    290                'is_citizen', 'last_name', 'passed_test',
    291                'say_hello', 'sex', 'year_of_birth',
    292            ]
    293        )
    294
    295
    296# Exceptions seen
    

    the test passes.


REFACTOR: make it better


  • I remove the commented lines and the second return statement from the can_get_license method in src/person/__init__.py

    15        self.is_citizen = is_citizen
    16        self.passed_test = passed_test
    17
    18    def can_get_license(self):
    19        return self.passed_test
    20
    21    def can_vote(self):
    
  • I add a call to can_get_license from test_jane, in tests/test_person.py

    125        jane = src.person.Person(
    126            first_name=first_name,
    127            last_name=last_name,
    128            sex=sex,
    129            year_of_birth=year_of_birth,
    130        )
    131        self.assertEqual(jane.can_vote(), True)
    132        self.assertEqual(jane.can_get_license(), True)
    133
    134    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 instance of the Person class for jane

    125        jane = src.person.Person(
    126            first_name=first_name,
    127            last_name=last_name,
    128            sex=sex,
    129            year_of_birth=year_of_birth,
    130            passed_test=True,
    131        )
    132        self.assertEqual(jane.can_vote(), True)
    133        self.assertEqual(jane.can_get_license(), True)
    

    the test passes.

  • I add a call to can_get_license from test_joe

     91        joe = src.person.Person(
     92            first_name=first_name,
     93            last_name=last_name,
     94            sex=sex,
     95            year_of_birth=year_of_birth,
     96        )
     97        self.assertEqual(joe.can_vote(), True)
     98        self.assertEqual(joe.can_get_license(), True)
     99
    100    def test_jane(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 change the expectation of the assertion for the can_get_license method in test_joe

     97        self.assertEqual(joe.can_vote(), True)
     98        # self.assertEqual(joe.can_get_license(), True)
     99        self.assertEqual(joe.can_get_license(), False)
    100
    101    def test_jane(self):
    

    the test passes.

  • I remove the commented line from test_joe

    91        joe = src.person.Person(
    92            first_name=first_name,
    93            last_name=last_name,
    94            sex=sex,
    95            year_of_birth=year_of_birth,
    96        )
    97        self.assertEqual(joe.can_vote(), True)
    98        self.assertEqual(joe.can_get_license(), False)
    
  • I add a git commit message in the other terminal

    git commit -am \
    'add passed_test attribute'
    

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


add condition to can_get_license

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

  • did the person pass the test?

  • is the person 18 or older?


RED: make it fail


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

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

    210    def test_underage_citizen(self):
    211        underage = src.person.Person(
    212            first_name='first_name',
    213            last_name='last_name',
    214            sex='M',
    215            year_of_birth=self.this_year-17,
    216            is_citizen=True,
    217            passed_test=True,
    218        )
    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):
    

    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 src/person/__init__.py

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

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

  • 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() -> False
    └── class Person:
        └── def can_get_license(self):
            └── if age < 18:
                └── return False
                return self.passed_test
    
    self.passed_test = True
    age < 18
    
    person.can_get_license() -> False
    └── class Person:
        └── def can_get_license(self):
            └── if age < 18:
                └── return False
                return self.passed_test
    
  • 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() -> False
      └── class Person:
          └── 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() -> True
      └── class Person:
          └── 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.

  • I add a git commit message in the other terminal

    git commit -am \
    'add condition to can_get_license'
    

extract age instance attribute

The can_get_license, can_vote and say_hello methods of the Person class all call the calculate_age function.

I can make an attribute to remove the repetition of the calls to the calculate_age function.


RED: make it fail



how to skip a test

I can use unittest.skip decorator to skip a test. The problem with skipping tests is that I will no longer know if the program does the thing the skipped tests were written for.


GREEN: make it pass


  • I add the unittest.skip decorator to test_when_person_is_older_than_120 with a note that it fails because the age is bigger than 120, in tests/test_person.py

    219        self.assertEqual(underage.can_vote(), False)
    220        self.assertEqual(underage.can_get_license(), False)
    221
    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        # ).say_hello() fails
    231        # because age > 120
    232
    233    def test_when_year_of_birth_is_the_future(self):
    
  • I remove the comments from test_when_person_is_older_than_120 because they are a repetition of the message in the unittest.skip decorator

    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    def test_when_year_of_birth_is_the_future(self):
    
  • I add the unittest.skip decorator to test_when_year_of_birth_is_the_future with a message that it will always fail since it uses a year of birth that is in the future

    231    @unittest.skip('fails because age < 0')
    232    def test_when_year_of_birth_is_the_future(self):
    233        src.person.Person(
    234            first_name='first_name',
    235            last_name='last_name',
    236            sex='F',
    237            year_of_birth=self.this_year+1,
    238        )
    239        # ).say_hello() fails
    240        # because age < 0
    
  • I remove the comments from test_when_year_of_birth_is_the_future

    231    @unittest.skip('fails because age < 0')
    232    def test_when_year_of_birth_is_the_future(self):
    233        src.person.Person(
    234            first_name='first_name',
    235            last_name='last_name',
    236            sex='F',
    237            year_of_birth=self.this_year+1,
    238        )
    239
    240    def test_when_year_of_birth_is_not_an_integer(self):
    
  • I add the unittest.skip decorator to test_when_year_of_birth_is_not_an_integer with a message

    240    @unittest.skip(
    241        'fails because year_of_birth is not an integer'
    242    )
    243    def test_when_year_of_birth_is_not_an_integer(self):
    244        src.person.Person(
    245            first_name='first_name',
    246            last_name='last_name',
    247            sex='M',
    248            # year_of_birth=None,     # fails
    249            # year_of_birth=2026.0,   # fails
    250            # year_of_birth='2026',   # fails
    251            # year_of_birth=(2026,),  # fails
    252        )
    253        # ).say_hello() fails
    254        # because year_of_birth is not an integer
    255
    256    def test_dir_person_class(self):
    
  • I remove the repeated message from test_when_year_of_birth_is_not_an_integer

    240    @unittest.skip(
    241        'fails because year_of_birth is not an integer'
    242    )
    243    def test_when_year_of_birth_is_not_an_integer(self):
    244        src.person.Person(
    245            first_name='first_name',
    246            last_name='last_name',
    247            sex='M',
    248            # year_of_birth=None,     # fails
    249            # year_of_birth=2026.0,   # fails
    250            # year_of_birth='2026',   # fails
    251            # year_of_birth=(2026,),  # fails
    252        )
    253
    254    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, 3 skipped in S.TUs ==========
    
  • I add age to the expectation of the assertion in test_dir_person_instance

    272    def test_dir_person_instance(self):
    273        self.assertEqual(
    274            dir(
    275                src.person.Person(
    276                    first_name='first_name',
    277                    last_name='last_name',
    278                    sex='M',
    279                    year_of_birth=2026,
    280                )
    281            ),
    282            [
    283                '__class__', '__delattr__', '__dict__',
    284                ...
    
    292                '__subclasshook__', '__weakref__',
    293                'age', 'can_get_license', 'can_vote',
    294                'first_name', 'is_citizen', 'last_name',
    295                'passed_test', 'say_hello', 'sex',
    296                'year_of_birth',
    297            ]
    298        )
    299
    300
    301# Exceptions seen
    

    the test passes.


REFACTOR: make it better


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

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

    the tests are still green.

  • I use self.age in the can_vote method

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

    still green.

  • I use self.age in the say_hello method

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

    green.

  • I remove the commented line from the say_hello method

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

    git commit -am \
    'extract age instance attribute'
    

extract check_age method

can_get_license and can_vote look the same, they both

  • return False if self.age is less than 18

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

if self.age < 18:
    return False
return something_else

I can make a method that removes the repetition.


RED: make it fail


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

16          self.passed_test = passed_test
17          self.age = calculate_age(year_of_birth)
18
19      def check_age(status):
20          if self.age < 18:
21              return False
22          return status
23
24      def can_get_license(self):

the terminal is my friend, and shows AssertionError

FAILED ...test_dir_person_class - AssertionError:
    Lists differ: ['__c[393 chars]ef__', 'can_get_li...
FAILED ...test_dir_person_instance - AssertionError:
    Lists differ: ['__c[434 chars]e', 'check_age', '...

because I added a new method.


GREEN: make it pass


  • I add check_age to the expectation of the assertion in test_dir_person_instance

    272    def test_dir_person_instance(self):
    273        self.assertEqual(
    274            dir(
    275                src.person.Person(
    276                    first_name='first_name',
    277                    last_name='last_name',
    278                    sex='M',
    279                    year_of_birth=2026,
    280                )
    281            ),
    282            [
    283                '__class__', '__delattr__', '__dict__',
    284                ...
    
    292                '__subclasshook__', '__weakref__',
    293                'age', 'can_get_license', 'can_vote',
    294                'check_age', 'first_name', 'is_citizen',
    295                'last_name', 'passed_test', 'say_hello',
    296                'sex', 'year_of_birth',
    297            ]
    298        )
    299
    300
    301# Exceptions seen
    302# AssertionError
    303# NameError
    304# TypeError
    305# AttributeError
    306# SyntaxError
    
  • I add check_age to the expectation of the assertion in test_dir_person_class in tests/test_person.py

    254    def test_dir_person_class(self):
    255        self.assertEqual(
    256            dir(src.person.Person),
    257            [
    258                '__class__', '__delattr__', '__dict__',
    259                ...
    
    267                '__subclasshook__', '__weakref__',
    268                'can_get_license', 'can_vote', 'check_age',
    269                'say_hello'
    270            ]
    271        )
    272
    273    def test_dir_person_instance(self):
    

    the test passes.


REFACTOR: make it better


  • I call the check_age method from the can_get_license method in src/person/__init__.py

    24    def can_get_license(self):
    25        return self.check_age(self.passed_test)
    26        # age = calculate_age(self.year_of_birth)
    27        # if age < 18:
    28        if self.age < 18:
    29            return False
    30        return self.passed_test
    31
    32    def can_vote(self):
    

    the terminal is my friend, and shows TypeError

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

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

  • I add self to the parentheses of the check_age method

    16        self.passed_test = passed_test
    17        self.age = calculate_age(year_of_birth)
    18
    19    def check_age(self, status):
    20        if self.age < 18:
    21            return False
    22        return status
    23
    24    def can_get_license(self):
    

    the test passes.

  • I remove the other statements from the can_get_license method

    24    def can_get_license(self):
    25        return self.check_age(self.passed_test)
    26
    27    def can_vote(self):
    
  • I call the check_age method from the can_vote method

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

    the tests are still green.

  • I remove the other statements from the can_vote method

    27    def can_vote(self):
    28        return self.check_age(self.is_citizen)
    29
    30    def say_hello(self):
    
  • I add a git commit message in the other terminal

    git commit -am \
    'extract check_age method'
    

close the project

  • I close tests/test_person.py and src/person/__init__.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.