how to make a person with a class


The factory and say_hello functions use three of the same inputs

  • first_name

  • last_name

  • year_of_birth

I want to give those values once, and get a representation for a person. I can do that with a class.

I think of classes as attributes (variables) and methods (functions) that belong together (a classification).


preview

I have these tests by the end of the chapter

person/tests/test_person.py
  1import src.person
  2
  3
  4def test_joe():
  5    first_name = 'joe'
  6    last_name = 'blow'
  7    sex = 'M'
  8    year_of_birth = 1996
  9
 10    reality = src.person.factory(
 11        first_name=first_name,
 12        last_name=last_name,
 13        sex=sex,
 14        year_of_birth=year_of_birth,
 15    )
 16    my_expectation = (
 17        f'{first_name}, {last_name},'
 18        f' {sex}, {year_of_birth}'
 19    )
 20    assert reality == my_expectation
 21
 22    reality = src.person.say_hello(
 23        first_name=first_name,
 24        last_name=last_name,
 25        year_of_birth=year_of_birth,
 26    )
 27    my_expectation = (
 28        f'Hello, my name is {first_name}'
 29        f' {last_name} and I am'
 30        f' {2026-year_of_birth}.'
 31    )
 32    assert reality == my_expectation
 33
 34    joe = src.person.Person(
 35        first_name=first_name,
 36        last_name=last_name,
 37        sex=sex,
 38        year_of_birth=year_of_birth,
 39    )
 40
 41    reality = joe.say_hello()
 42    assert reality == my_expectation
 43
 44
 45def test_jane():
 46    first_name = 'jane'
 47    last_name = 'doe'
 48    sex = 'F'
 49    year_of_birth = 1991
 50
 51    reality = src.person.factory(
 52        first_name=first_name,
 53        last_name=last_name,
 54        sex=sex,
 55        year_of_birth=year_of_birth,
 56    )
 57    my_expectation = (
 58        f'{first_name}, {last_name},'
 59        f' {sex}, {year_of_birth}'
 60    )
 61    assert reality == my_expectation
 62
 63    reality = src.person.say_hello(
 64        first_name=first_name,
 65        last_name=last_name,
 66        year_of_birth=year_of_birth,
 67    )
 68    my_expectation = (
 69        f'Hello, my name is {first_name}'
 70        f' {last_name} and I am'
 71        f' {2026-year_of_birth}.'
 72    )
 73    assert reality == my_expectation
 74
 75    jane = src.person.Person(
 76        first_name=first_name,
 77        last_name=last_name,
 78        sex=sex,
 79        year_of_birth=year_of_birth,
 80    )
 81
 82    reality = jane.say_hello()
 83    assert reality == my_expectation
 84
 85
 86def test_john():
 87    first_name = 'john'
 88    last_name = 'smith'
 89    sex = 'M'
 90    year_of_birth = 1580
 91
 92    reality = src.person.factory(
 93        first_name=first_name,
 94        last_name=last_name,
 95        sex=sex,
 96        year_of_birth=year_of_birth,
 97    )
 98    my_expectation = (
 99        f'{first_name}, {last_name},'
100        f' {sex}, {year_of_birth}'
101    )
102    assert reality == my_expectation
103
104    reality = src.person.say_hello(
105        first_name=first_name,
106        last_name=last_name,
107        year_of_birth=year_of_birth,
108    )
109    my_expectation = (
110        f'Hello, my name is {first_name}'
111        f' {last_name} and I am'
112        f' {2026-year_of_birth}.'
113    )
114    assert reality == my_expectation
115
116    john = src.person.Person(
117        first_name=first_name,
118        last_name=last_name,
119        sex=sex,
120        year_of_birth=year_of_birth,
121    )
122
123    reality = john.say_hello()
124    assert reality == my_expectation
125
126
127def test_mary():
128    first_name = 'mary'
129    last_name = 'public'
130    sex = 'F'
131    year_of_birth = 2000
132
133    reality = src.person.factory(
134        first_name=first_name,
135        last_name=last_name,
136        sex=sex,
137        year_of_birth=year_of_birth,
138    )
139    my_expectation = (
140        f'{first_name}, {last_name},'
141        f' {sex}, {year_of_birth}'
142    )
143    assert reality == my_expectation
144
145    reality = src.person.say_hello(
146        first_name=first_name,
147        last_name=last_name,
148        year_of_birth=year_of_birth,
149    )
150    my_expectation = (
151        f'Hello, my name is {first_name}'
152        f' {last_name} and I am'
153        f' {2026-year_of_birth}.'
154    )
155    assert reality == my_expectation
156
157    mary = src.person.Person(
158        first_name=first_name,
159        last_name=last_name,
160        sex=sex,
161        year_of_birth=year_of_birth,
162    )
163
164    reality = mary.say_hello()
165    assert reality == my_expectation
166
167
168def test_dir_person_class():
169    reality = dir(src.person.Person)
170    my_expectation = [
171        '__class__',
172        '__delattr__',
173        '__dict__',
174        '__dir__',
175        '__doc__',
176        '__eq__',
177        '__firstlineno__',
178        '__format__',
179        '__ge__',
180        '__getattribute__',
181        '__getstate__',
182        '__gt__',
183        '__hash__',
184        '__init__',
185        '__init_subclass__',
186        '__le__',
187        '__lt__',
188        '__module__',
189        '__ne__',
190        '__new__',
191        '__reduce__',
192        '__reduce_ex__',
193        '__repr__',
194        '__setattr__',
195        '__sizeof__',
196        '__static_attributes__',
197        '__str__',
198        '__subclasshook__',
199        '__weakref__',
200        'say_hello'
201    ]
202    assert reality == my_expectation
203
204
205def test_dir_person_instance():
206    an_instance_of_person = src.person.Person(
207        first_name='first_name',
208        last_name='last_name',
209        sex='M',
210        year_of_birth=2026,
211    )
212
213    reality = dir(an_instance_of_person)
214    my_expectation = [
215        '__class__',
216        '__delattr__',
217        '__dict__',
218        '__dir__',
219        '__doc__',
220        '__eq__',
221        '__firstlineno__',
222        '__format__',
223        '__ge__',
224        '__getattribute__',
225        '__getstate__',
226        '__gt__',
227        '__hash__',
228        '__init__',
229        '__init_subclass__',
230        '__le__',
231        '__lt__',
232        '__module__',
233        '__ne__',
234        '__new__',
235        '__reduce__',
236        '__reduce_ex__',
237        '__repr__',
238        '__setattr__',
239        '__sizeof__',
240        '__static_attributes__',
241        '__str__',
242        '__subclasshook__',
243        '__weakref__',
244        'first_name',
245        'last_name',
246        'say_hello',
247        'sex',
248        'year_of_birth',
249    ]
250    assert reality == my_expectation
251
252
253# Exceptions seen
254# AssertionError
255# NameError
256# TypeError
257# AttributeError
258# SyntaxError

open the project

  • I open a terminal

  • I change directory to the project

    cd person
    

    the terminal shows I am in the person folder

    .../pumping_python/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%]
    
    =================== 4 passed in A.BCs ====================
    

test Person class

I made a function that makes a string to represent a person when I give it first_name, last_name, sex and year_of_birth. I can also represent a person with a class because it is attributes and methods that belong together.


RED: make it fail


I make a copy of a class to represent joe in test_joe in test_person.py

 4def test_joe():
 5    first_name = 'joe'
 6    last_name = 'blow'
 7    sex = 'M'
 8    year_of_birth = 1996
 9
10    reality = src.person.factory(
11        first_name=first_name,
12        last_name=last_name,
13        sex=sex,
14        year_of_birth=year_of_birth,
15    )
16    my_expectation = (
17        f'{first_name}, {last_name},'
18        f' {sex}, {year_of_birth}'
19    )
20    assert reality == my_expectation
21
22    reality = src.person.say_hello(
23        first_name=first_name,
24        last_name=last_name,
25        year_of_birth=year_of_birth,
26    )
27    my_expectation = (
28        f'Hello, my name is {first_name}'
29        f' {last_name} and I am'
30        f' {2026-year_of_birth}.'
31    )
32    assert reality == my_expectation
33
34    joe = Person(
35        first_name=first_name,
36        last_name=last_name,
37        sex=sex,
38        year_of_birth=year_of_birth,
39    )
40
41
42def test_jane():

the terminal is my friend, and shows NameError

NameError: name 'Person' is not defined

because there is no definition for Person in test_person.py.


GREEN: make it pass


  • I add a class definition for Person

    1import src.person
    2
    3
    4class Person:
    5
    6    pass
    7
    8
    9def test_joe():
    
    • I can make a class with the pass keyword.

    • The terminal is my friend, and shows TypeError

      TypeError: Person() takes no arguments
      

      because this happens when joe = Person(first_name=first_name, last_name=last_name, sex=sex, year_of_birth=year_of_birth) runs

      joe = Person(
          first_name=first_name,
          last_name=last_name,
          sex=sex,
          year_of_birth=year_of_birth,
      )
      └── Person(
              first_name=first_name,
              last_name=last_name,
              sex=sex,
              year_of_birth=year_of_birth,
          ) # has no constructor method
      

      which raises TypeError since classes do not take arguments like a function without a method that handles those arguments and I called this one with four arguments.


the constructor method

A constructor method is used to define what happens when an instance (a copy) of a class is made.

  • I add a constructor method to the Person class so it can take arguments

     4class Person:
     5
     6    # pass
     7    def __init__():
     8        return None
     9
    10
    11def test_joe():
    

    the terminal is my friend, and shows TypeError

    TypeError:
        Person.__init__() got
        an unexpected keyword argument 'first_name'
    
    • Here is what is happens when the joe instance of the Person class runs

      joe = Person(
          first_name=first_name,
          last_name=last_name,
          sex=sex,
          year_of_birth=year_of_birth,
      )
      └── Person.__init__(
              first_name='joe',
              last_name='blow',
              sex='M',
              year_of_birth=1996,
          )
      

      which raises TypeError since the __init__ method got called with a name (first_name) that is not in the parentheses of its definition.

    • I am violating the method signature when I call it in a way that it was not designed to be called.

  • I add the name in parentheses so that the __init__ constructor method can take input

     4class Person:
     5
     6    # pass
     7    # def __init__():
     8    def __init__(first_name):
     9        return None
    10
    11
    12def test_joe():
    

    the terminal is my friend, and shows TypeError

    TypeError:
        Person.__init__() got
        multiple values for argument 'first_name'
    

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

    The test calls the function with four keyword arguments (first_name, last_name, sex and year_of_birth'). How does Python know which value to use for the first argument if I use the position and a keyword?

  • I add self as the first argument

     4class Person:
     5
     6    # pass
     7    # def __init__():
     8    # def __init__(first_name):
     9    def __init__(self, first_name):
    10        return None
    11
    12
    13def test_joe():
    
    • self is Python convention, I can use any name I want.

    • self is the instance of the class.

    • The terminal is my friend, and shows TypeError

      TypeError:
          Person.__init__() got
          an unexpected keyword argument 'last_name'.
          Did you mean 'first_name'?
      

      because this happens when joe = Person(first_name=first_name, last_name=last_name, sex=sex, year_of_birth=year_of_birth) runs

      joe = Person(
          first_name=first_name,
          last_name=last_name,
          sex=sex,
          year_of_birth=year_of_birth,
      )
      └── Person.__init__(
              self,
              first_name='joe',
              last_name='blow',    # not in definition
              sex='M',
              year_of_birth=1996,
          )
      

      which raises TypeError since the __init__ method got called with a name (first_name) that is not in the parentheses of its definition.

    • self is the instance of the class.

    • I am violating the method signature when I call it in a way that it was not designed to be called.

    • I have seen this before, so far it is the same as making the factory function.

  • I add last_name to the definition of __init__

     4class Person:
     5
     6    # pass
     7    # def __init__():
     8    # def __init__(first_name):
     9    # def __init__(self, first_name):
    10    def __init__(self, first_name, last_name):
    11        return None
    12
    13
    14def test_joe():
    

    the terminal is my friend, and shows TypeError

    TypeError:
        Person.__init__() got
        an unexpected keyword argument 'sex'
    
    • because this happens when joe = Person(first_name=first_name, last_name=last_name, sex=sex, year_of_birth=year_of_birth) runs

      joe = Person(
          first_name=first_name,
          last_name=last_name,
          sex=sex,
          year_of_birth=year_of_birth,
      )
      └── Person.__init__(
              self,
              first_name='joe',
              last_name='blow',
              sex='M',             # not in definition
              year_of_birth=1996,
          )
      

      which raises TypeError since the __init__ method got called with a name (sex) that is not in the parentheses of its definition.

    • self is the instance of the class.

    • I am violating the method signature when I call it in a way that it was not designed to be called.

    • Still the same as making the factory function.

  • I add sex to the definition of the __init__ method

     4class Person:
     5
     6    # pass
     7    # def __init__():
     8    # def __init__(first_name):
     9    # def __init__(self, first_name):
    10    # def __init__(self, first_name, last_name):
    11    def __init__(
    12        self, first_name, last_name,
    13        sex,
    14    ):
    15        return None
    16
    17
    18def test_joe():
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.__init__() got
               an unexpected keyword argument 'year_of_birth'
    
    • because this happens when joe = Person(first_name=first_name, last_name=last_name, sex=sex, year_of_birth=year_of_birth) runs

      joe = Person(
          first_name=first_name,
          last_name=last_name,
          sex=sex,
          year_of_birth=year_of_birth,
      )
      └── Person.__init__(
              self,
              first_name='joe',
              last_name='blow',
              sex='M',
              year_of_birth=1996,  # not in definition
          )
      

      which raises TypeError because the __init__ method got called with a name (year_of_birth) that is not in the parentheses of its definition.

    • self is the instance of the class.

    • I am violating the method signature when I call it in a way that it was not designed to be called.

    • Same as with the factory function.

  • I add year_of_birth to the definition of the __init__ constructor method

     4class Person:
     5
     6    # pass
     7    # def __init__():
     8    # def __init__(first_name):
     9    # def __init__(self, first_name):
    10    # def __init__(self, first_name, last_name):
    11    def __init__(
    12        self, first_name, last_name,
    13        # sex,
    14        sex, year_of_birth
    15    ):
    16        return None
    17
    18
    19def test_joe():
    

    the test passes.


REFACTOR: make it better


  • I remove the commented lines

     4class Person:
     5
     6    def __init__(
     7        self, first_name, last_name,
     8        sex, year_of_birth,
     9    ):
    10        return None
    11
    12
    13def test_joe():
    
  • I open a new terminal then change directories to person

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

    git commit -am 'add Person class'
    

test say_hello method

I made a person say hi with a function, I can also do the same thing with a class because it is attributes and methods that belong together.


RED: make it fail


  • I add an assertion with a call to the say_hello function with the attributes of joe in test_joe

    43    joe = Person(
    44        first_name=first_name,
    45        last_name=last_name,
    46        sex=sex,
    47        year_of_birth=year_of_birth,
    48    )
    49
    50    reality = src.person.say_hello(
    51        first_name=joe.first_name,
    52        last_name=joe.last_name,
    53        year_of_birth=joe.year_of_birth,
    54    )
    55    assert reality == my_expectation
    56
    57
    58def test_jane():
    

    the terminal is my friend, and shows AttributeError

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

    because there is nothing named first_name in the Person class


GREEN: make it pass


  • I add self.first_name to the definition of the __init__ constructor method

     4class Person:
     5
     6    def __init__(
     7        self, first_name, last_name,
     8        sex, year_of_birth,
     9    ):
    10        self.first_name
    11        return None
    12
    13
    14def test_joe():
    

    the terminal still shows AttributeError because this is just a reference to the name, not a definition.

  • I point self.first_name to the value for first_name when the __init__ method is called

     4class Person:
     5
     6    def __init__(
     7        self, first_name, last_name,
     8        sex, year_of_birth,
     9    ):
    10        # self.first_name
    11        self.first_name = first_name
    12        return None
    13
    14
    15def test_joe():
    

    the terminal is my friend, and shows AttributeError

    AttributeError: 'Person' object
                    has no attribute 'last_name'.
                    Did you mean: 'first_name'?
    
  • I add self.last_name and point it to the value for last_name when the __init__ method is called

     4class Person:
     5
     6    def __init__(
     7        self, first_name, last_name,
     8        sex, year_of_birth,
     9    ):
    10        # self.first_name
    11        self.first_name = first_name
    12        self.last_name = last_name
    13        return None
    14
    15
    16def test_joe():
    

    the terminal is my friend, and shows AttributeError

    AttributeError: 'Person' object
                    has no attribute 'year_of_birth'
    
  • I add self.year_of_birth and point it to the value for year_of_birth when the __init__ constructor method is called

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

    the test passes, because

    • given

      first_name = 'joe'
      last_name = 'blow'
      sex = 'M'
      year_of_birth = 1996
      
      joe = Person(
          first_name=first_name,
          last_name=last_name,
          sex=sex,
          year_of_birth=year_of_birth,
      )
      └── Person.__init__(
              self,
              first_name='joe',
              last_name='blow',
              sex='M',
              year_of_birth=1996,
          )
          ├── self.first_name = 'joe'
          ├── self.last_name = 'blow'
          └── self.year_of_birth = 1996
      

      self is the instance of the class.

      reality = src.person.say_hello(
          first_name=joe.first_name,
          last_name=joe.last_name,
          year_of_birth=joe.year_of_birth,
      )
      └── src.person.say_hello(
              first_name='joe',
              last_name='blow',
              year_of_birth=1996,
          )
      

      Python follows this path

      src.person.say_hello
      src
      └── person.py
          └── def say_hello(
                  first_name, last_name, year_of_birth,
              ):
              └── return (
                      f'Hello, my name is {first_name}'
                      f' {last_name} and I am'
                      f' {2026-year_of_birth}.'
                  )
      

    and the result is 'Hello, my name is joe blow and I am 30.'


REFACTOR: make it better


  • I remove the commented line

     4class Person:
     5
     6    def __init__(
     7        self, first_name, last_name,
     8        sex, year_of_birth,
     9    ):
    10        self.first_name = first_name
    11        self.last_name = last_name
    12        self.year_of_birth = year_of_birth
    13        return None
    14
    15
    16def test_joe():
    
  • I change the call to src.person.say_hello in test_joe to a call to the say_hello method of the Person class

    47    joe = 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 = src.person.say_hello(
    55    reality = Person.say_hello(
    56        first_name=joe.first_name,
    57        last_name=joe.last_name,
    58        year_of_birth=joe.year_of_birth,
    59    )
    60    assert reality == my_expectation
    61
    62
    63def test_jane():
    

    the terminal is my friend, and shows AttributeError

    AttributeError: type object 'Person'
                    has no attribute 'say_hello'
    

    because the test calls the say_hello method which does not yet exist in the Person class.

  • I add a method definition for it to the Person class

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

    the terminal is my friend, and shows TypeError

    TypeError: Person.say_hello() got
               an unexpected keyword argument 'first_name'
    

    because the say_hello method got called with a name (first_name) that is not in the parentheses of its definition..

  • I add first_name to the method definition

    15    # def say_hello():
    16    def say_hello(first_name):
    17        return None
    18
    19
    20def test_joe():
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.say_hello() got
               an unexpected keyword argument 'last_name'.
               Did you mean 'first_name'?
    

    because the say_hello method got called with a name (first_name) that is not in the parentheses of its definition.

  • I add last_name to the method definition

    15    # def say_hello():
    16    # def say_hello(first_name):
    17    def say_hello(first_name, last_name):
    18        return None
    19
    20
    21def test_joe():
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.say_hello() got
               an unexpected keyword argument 'year_of_birth'
    

    because the say_hello method got called with a name (year_of_birth) that is not in the parentheses of its definition.

  • I add year_of_birth to the method definition

    15    # def say_hello():
    16    # def say_hello(first_name):
    17    # def say_hello(first_name, last_name):
    18    def say_hello(first_name, last_name, year_of_birth):
    19        return None
    20
    21
    22def test_joe():
    

    the terminal is my friend, and shows AssertionError

    AssertionError: assert None
        == 'Hello, my name is joe blow and I am 30.'
    
  • I change the return statement to match

    15    # def say_hello():
    16    # def say_hello(first_name):
    17    # def say_hello(first_name, last_name):
    18    def say_hello(first_name, last_name, year_of_birth):
    19        # return None
    20        return 'Hello, my name is joe blow and I am 30.'
    21
    22
    23def test_joe():
    

    the test passes.

  • I add a call to the Person class and say_hello method in test_jane

     69def test_jane():
     70    first_name = 'jane'
     71    last_name = 'doe'
     72    sex = 'F'
     73    year_of_birth = 1991
     74
     75    reality = src.person.factory(
     76        first_name=first_name,
     77        last_name=last_name,
     78        sex=sex,
     79        year_of_birth=year_of_birth,
     80    )
     81    my_expectation = (
     82        f'{first_name}, {last_name},'
     83        f' {sex}, {year_of_birth}'
     84    )
     85    assert reality == my_expectation
     86
     87    reality = src.person.say_hello(
     88        first_name=first_name,
     89        last_name=last_name,
     90        year_of_birth=year_of_birth,
     91    )
     92    my_expectation = (
     93        f'Hello, my name is {first_name}'
     94        f' {last_name} and I am'
     95        f' {2026-year_of_birth}.'
     96    )
     97    assert reality == my_expectation
     98
     99    jane = Person(
    100        first_name=first_name,
    101        last_name=last_name,
    102        sex=sex,
    103        year_of_birth=year_of_birth,
    104    )
    105
    106    reality = Person.say_hello(
    107        first_name=jane.first_name,
    108        last_name=jane.last_name,
    109        year_of_birth=jane.year_of_birth,
    110    )
    111    assert reality == my_expectation
    112
    113
    114def test_john():
    

    the terminal is my friend, and shows AssertionError

    AssertionError: assert 'Hello, my name ... and I am 30.'
                        == 'Hello, my name ... and I am 35.'
    
  • I change the return statement to an f-string with the input like the say_hello function in person.py in the src folder

    15    # def say_hello():
    16    # def say_hello(first_name):
    17    # def say_hello(first_name, last_name):
    18    def say_hello(first_name, last_name, year_of_birth):
    19        # return None
    20        # return 'Hello, my name is joe blow and I am 30.'
    21        return (
    22            f'Hello, my name is {first_name}'
    23            f' {last_name} and I am'
    24            f' {2026-year_of_birth}.'
    25        )
    26
    27
    28def test_joe():
    

    the test passes. This is still repeating the values for first_name, last_name and year_of_birth.

  • I change the call to the say_hello method in test_joe to take in an instance (copy) of the Person class since it will already have the attributes

    65    # reality = src.person.say_hello(
    66    reality = Person.say_hello(
    67        person=joe,
    68        first_name=joe.first_name,
    69        last_name=joe.last_name,
    70        year_of_birth=joe.year_of_birth,
    71    )
    72    assert reality == my_expectation
    73
    74
    75def test_jane():
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.say_hello() got
               an unexpected keyword argument 'person'
    

    because the say_hello method got called with a name (person) that is not in the parentheses of its definition.

  • I add person to the method definition for say_hello

    15    # def say_hello():
    16    # def say_hello(first_name):
    17    # def say_hello(first_name, last_name):
    18    # def say_hello(first_name, last_name, year_of_birth):
    19    def say_hello(
    20        person, first_name, last_name, year_of_birth,
    21    ):
    22        # return None
    23        # return 'Hello, my name is joe blow and I am 30.'
    24        return (
    25            f'Hello, my name is {first_name}'
    26            f' {last_name} and I am'
    27            f' {2026-year_of_birth}.'
    28        )
    29
    30
    31def test_joe():
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.say_hello() missing
               1 required positional argument: 'person'
    

    I have to make the same change to test_jane

  • I add the person keyword argument to the call to the say_hello method in test_jane

    116    reality = Person.say_hello(
    117        person=jane,
    118        first_name=jane.first_name,
    119        last_name=jane.last_name,
    120        year_of_birth=jane.year_of_birth,
    121    )
    122    assert reality == my_expectation
    123
    124
    125def test_john():
    

    the test passes.

  • I change the return statement of the say_hello method to use the attributes of the class instance it receives as input

    15    # def say_hello():
    16    # def say_hello(first_name):
    17    # def say_hello(first_name, last_name):
    18    # def say_hello(first_name, last_name, year_of_birth):
    19    def say_hello(
    20        person, first_name, last_name, year_of_birth,
    21    ):
    22        # return None
    23        # return 'Hello, my name is joe blow and I am 30.'
    24        return (
    25            # f'Hello, my name is {first_name}'
    26            # f' {last_name} and I am'
    27            # f' {2026-year_of_birth}.'
    28            f'Hello, my name is {person.first_name}'
    29            f' {person.last_name} and I am'
    30            f' {2026-person.year_of_birth}.'
    31        )
    32
    33
    34def test_joe():
    

    the test passes because

    • given

      first_name = 'joe'
      last_name = 'blow'
      sex = 'M'
      year_of_birth = 1996
      
      joe = Person(
          first_name=first_name,
          last_name=last_name,
          sex=sex,
          year_of_birth=year_of_birth,
      )
      └── Person.__init__(
              self,
              first_name='joe',
              last_name='blow',
              sex='M',
              year_of_birth=1996,
          )
          ├── self.first_name = 'joe'
          ├── self.last_name = 'blow'
          └── self.year_of_birth = 1996
      

      self is the instance of the class aka joe.

      reality = Person.say_hello(
          person=joe,
          first_name=joe.first_name,
          last_name=joe.last_name,
          year_of_birth=joe.year_of_birth,
      )
      └── Person.say_hello(
              person=joe,
              first_name='joe',
              last_name='blow',
              year_of_birth=1996,
          )
          └── return (
                  f'Hello, my name is {person.first_name}'
                  f' {person.last_name} and I am'
                  f' {2026-person.year_of_birth}.'
              )
              return (
                  f'Hello, my name is {joe.first_name}'
                  f' {joe.last_name} and I am'
                  f' {2026-joe.year_of_birth}.'
              )
      

    and the result is 'Hello, my name is joe blow and I am 30.'

  • I remove the first_name, last_name and year_of_birth arguments from the call in test_joe since they are repetitions of the class attributes

    71    # reality = src.person.say_hello(
    72    reality = Person.say_hello(
    73        person=joe,
    74        # first_name=joe.first_name,
    75        # last_name=joe.last_name,
    76        # year_of_birth=joe.year_of_birth,
    77    )
    78    assert reality == my_expectation
    79
    80
    81def test_jane():
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.say_hello() missing
               3 required positional arguments:
               'first_name', 'last_name', and 'year_of_birth'
    
  • I remove first_name, last_name and year_of_birth from the definition of the say_hello method

    15    # def say_hello():
    16    # def say_hello(first_name):
    17    # def say_hello(first_name, last_name):
    18    # def say_hello(first_name, last_name, year_of_birth):
    19    def say_hello(
    20        # person, first_name, last_name, year_of_birth,
    21        person
    22    ):
    23        # return None
    24        # return 'Hello, my name is joe blow and I am 30.'
    25        return (
    26            # f'Hello, my name is {first_name}'
    27            # f' {last_name} and I am'
    28            # f' {2026-year_of_birth}.'
    29            f'Hello, my name is {person.first_name}'
    30            f' {person.last_name} and I am'
    31            f' {2026-person.year_of_birth}.'
    32        )
    33
    34
    35def test_joe():
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.say_hello() got
               an unexpected keyword argument 'first_name'
    

    because the say_hello method got called with a name (first_name) that is not in the parentheses of its definition.

  • I remove the first_name, last_name and year_of_birth arguments from the call in test_jane

    119    reality = Person.say_hello(
    120        person=jane,
    121        # first_name=jane.first_name,
    122        # last_name=jane.last_name,
    123        # year_of_birth=jane.year_of_birth,
    124    )
    125    assert reality == my_expectation
    126
    127
    128def test_john():
    

    the test passes. This is still a repetition. I give an instance (copy) of the Person class as input to the say_hello method of the Person class (Person.say_hello).

  • I change the call to the say_hello method in test_jane because the say_hello method is in the Person class so its copies also have the say_hello method

    119    # reality = Person.say_hello(
    120    reality = jane.say_hello(
    121        person=jane,
    122        # first_name=jane.first_name,
    123        # last_name=jane.last_name,
    124        # year_of_birth=jane.year_of_birth,
    125    )
    126    assert reality == my_expectation
    127
    128
    129def test_john():
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.say_hello() got
               multiple values for argument 'person'
    

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


what is the staticmethod decorator?

  • I can use the staticmethod decorator if I do not want to add self to the method definition whenit does not use anything that belongs to the class that way I am not sending more information than what the method needs. I add @staticmethod to the say_hello method

    15    # def say_hello():
    16    # def say_hello(first_name):
    17    # def say_hello(first_name, last_name):
    18    # def say_hello(first_name, last_name, year_of_birth):
    19    @staticmethod
    20    def say_hello(
    21        # person, first_name, last_name, year_of_birth,
    22        person
    23    ):
    24        # return None
    25        # return 'Hello, my name is joe blow and I am 30.'
    26        return (
    27            # f'Hello, my name is {first_name}'
    28            # f' {last_name} and I am'
    29            # f' {2026-year_of_birth}.'
    30            f'Hello, my name is {person.first_name}'
    31            f' {person.last_name} and I am'
    32            f' {2026-person.year_of_birth}.'
    33        )
    34
    35
    36def test_joe():
    

    the test passes.

  • I change the call to Person.say_hello in test_joe because the say_hello method is in the Person class, there is no need for it to take a copy of the Person class as input since it should be able to use its own attributes

    73    # reality = src.person.say_hello(
    74    # reality = Person.say_hello(
    75    reality = joe.say_hello(
    76        # person=joe,
    77        # first_name=joe.first_name,
    78        # last_name=joe.last_name,
    79        # year_of_birth=joe.year_of_birth,
    80    )
    81    assert reality == my_expectation
    82
    83
    84def test_jane():
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.say_hello() missing
               1 required positional argument: 'person'
    
  • I make person optional in the say_hello method

    15# def say_hello():
    16# def say_hello(first_name):
    17# def say_hello(first_name, last_name):
    18# def say_hello(first_name, last_name, year_of_birth):
    19@staticmethod
    20def say_hello(
    21    # person, first_name, last_name, year_of_birth,
    22    # person
    23    person=None
    24):
    

    the terminal is my friend, and shows AttributeError

    AttributeError: 'NoneType' object
                    has no attribute 'first_name'
    
  • I change person. to self. in the return statement

    15    # def say_hello():
    16    # def say_hello(first_name):
    17    # def say_hello(first_name, last_name):
    18    # def say_hello(first_name, last_name, year_of_birth):
    19    @staticmethod
    20    def say_hello(
    21        # person, first_name, last_name, year_of_birth,
    22        # person
    23        person=None
    24    ):
    25        # return None
    26        # return 'Hello, my name is joe blow and I am 30.'
    27        return (
    28            # f'Hello, my name is {first_name}'
    29            # f' {last_name} and I am'
    30            # f' {2026-year_of_birth}.'
    31            # f'Hello, my name is {person.first_name}'
    32            # f' {person.last_name} and I am'
    33            # f' {2026-person.year_of_birth}.'
    34            f'Hello, my name is {self.first_name}'
    35            f' {self.last_name} and I am'
    36            f' {2026-self.year_of_birth}.'
    37        )
    38
    39
    40def test_joe():
    

    the terminal is my friend, and shows NameError

    NameError: name 'self' is not defined
    
  • I add self to the parentheses

    15    # def say_hello():
    16    # def say_hello(first_name):
    17    # def say_hello(first_name, last_name):
    18    # def say_hello(first_name, last_name, year_of_birth):
    19    @staticmethod
    20    def say_hello(
    21        # person, first_name, last_name, year_of_birth,
    22        # person
    23        # person=None
    24        self, person=None
    25    ):
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.say_hello() missing
               1 required positional argument: 'self'
    
  • I remove the staticmethod decorator because I no longer need it since the say_hello method is using class attributes

    15    # def say_hello():
    16    # def say_hello(first_name):
    17    # def say_hello(first_name, last_name):
    18    # def say_hello(first_name, last_name, year_of_birth):
    19    # @staticmethod
    20    def say_hello(
    21        # person, first_name, last_name, year_of_birth,
    22        # person
    23        # person=None
    24        self, person=None
    25    ):
    

    the test passes because

    • given

      first_name = 'joe'
      last_name = 'blow'
      sex = 'M'
      year_of_birth = 1996
      
      joe = Person(
          first_name=first_name,
          last_name=last_name,
          sex=sex,
          year_of_birth=year_of_birth,
      )
      └── Person.__init__(
              self,
              first_name='joe',
              last_name='blow',
              sex='M',
              year_of_birth=1996,
          )
          ├── self.first_name = 'joe'
          ├── self.last_name = 'blow'
          └── self.year_of_birth = 1996
      

      self is the instance of the class aka joe.

      reality = joe.say_hello()
      └── return (
              f'Hello, my name is {self.first_name}'
              f' {self.last_name} and I am'
              f' {2026-self.year_of_birth}.'
          )
          # inside joe, self == joe
          return (
              f'Hello, my name is {joe.first_name}'
              f' {joe.last_name} and I am'
              f' {2026-joe.year_of_birth}.'
          )
      

      and the result is 'Hello, my name is joe blow and I am 30.'

    • a simple way to think of joe.say_hello() is

      joe.say_hello() == Person().say_hello()
      joe.say_hello() == joe.say_hello(Person())
      joe.say_hello() == joe.say_hello(joe)
      

      I do not need to pass joe as input to the say_hello method since it is self.

  • I remove person=jane from the call to the say_hello method in test_jane because the say_hello method is in the Person class

    126    # reality = Person.say_hello(
    127    reality = jane.say_hello(
    128        # person=jane,
    129        # first_name=jane.first_name,
    130        # last_name=jane.last_name,
    131        # year_of_birth=jane.year_of_birth,
    132    )
    133    assert reality == my_expectation
    134
    135
    136def test_john():
    

    the test is still green.

  • I add an assertion to test_john for the say_hello method

    154    reality = src.person.say_hello(
    155        first_name=first_name,
    156        last_name=last_name,
    157        year_of_birth=year_of_birth,
    158    )
    159    my_expectation = (
    160        f'Hello, my name is {first_name}'
    161        f' {last_name} and I am'
    162        f' {2026-year_of_birth}.'
    163    )
    164    assert reality == my_expectation
    165
    166    john = Person(
    167        first_name=first_name,
    168        last_name=last_name,
    169        sex=sex,
    170        year_of_birth=year_of_birth,
    171    )
    172
    173    reality = john.say_hello()
    174    assert reality == None
    175
    176
    177def test_mary():
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        assert 'Hello, my name is john smith and I am 446.'
            == None
    
  • I change my expectation to match reality

    166    john = Person(
    167        first_name=first_name,
    168        last_name=last_name,
    169        sex=sex,
    170        year_of_birth=year_of_birth,
    171    )
    172
    173    reality = john.say_hello()
    174    # assert reality == None
    175    assert reality == my_expectation
    176
    177
    178def test_mary():
    

    the test passes.

  • I add an assertion to test_mary for the say_hello method

    196    reality = src.person.say_hello(
    197        first_name=first_name,
    198        last_name=last_name,
    199        year_of_birth=year_of_birth,
    200    )
    201    my_expectation = (
    202        f'Hello, my name is {first_name}'
    203        f' {last_name} and I am'
    204        f' {2026-year_of_birth}.'
    205    )
    206    assert reality == my_expectation
    207
    208    mary = Person(
    209        first_name=first_name,
    210        last_name=last_name,
    211        sex=sex,
    212        year_of_birth=year_of_birth,
    213    )
    214
    215    reality = mary.say_hello()
    216    assert reality == None
    217
    218
    219# Exceptions seen
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        assert 'Hello, my name is mary public and I am 26.'
             == None
    
  • I change my expectation to match reality

    208    mary = Person(
    209        first_name=first_name,
    210        last_name=last_name,
    211        sex=sex,
    212        year_of_birth=year_of_birth,
    213    )
    214
    215    reality = mary.say_hello()
    216    # assert reality == None
    217    assert reality == my_expectation
    218
    219
    220# Exceptions seen
    

    the test passes because

    • given

      first_name = 'mary'
      last_name = 'public'
      sex = 'F'
      year_of_birth = 2000
      
      mary = Person(
          first_name=first_name,
          last_name=last_name,
          sex=sex,
          year_of_birth=year_of_birth,
      )
      └── Person.__init__(
              self,
              first_name='mary',
              last_name='public',
              sex='F',
              year_of_birth=2000,
          )
          ├── self.first_name = 'mary'
          ├── self.last_name = 'public'
          └── self.year_of_birth = 2000
      

      self is the instance of the class aka mary.

      reality = mary.say_hello()
      └── return (
              f'Hello, my name is {self.first_name}'
              f' {self.last_name} and I am'
              f' {2026-self.year_of_birth}.'
          )
          # inside mary, self == mary
          return (
              f'Hello, my name is {mary.first_name}'
              f' {mary.last_name} and I am'
              f' {2026-mary.year_of_birth}.'
          )
      

      and the result is 'Hello, my name is mary public and I am 26.'

    • a simple way to think of mary.say_hello() is

      mary.say_hello() == Person().say_hello()
      mary.say_hello() == mary.say_hello(Person())
      mary.say_hello() == mary.say_hello(mary)
      

      I do not need to pass mary as input to the say_hello method since it is self.

  • I add a git commit message in the other terminal

    git commit -am 'add say_hello method'
    

separate and equal Person class

RED: make it fail


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

  • I change mary in test_mary to be the result of a call to the Person class of the person module in the src folder instead of a call to the Person class in test_person.py

    196    reality = src.person.say_hello(
    197        first_name=first_name,
    198        last_name=last_name,
    199        year_of_birth=year_of_birth,
    200    )
    201    my_expectation = (
    202        f'Hello, my name is {first_name}'
    203        f' {last_name} and I am'
    204        f' {2026-year_of_birth}.'
    205    )
    206    assert reality == my_expectation
    207
    208    # mary = Person(
    209    mary = src.person.Person(
    210        first_name=first_name,
    211        last_name=last_name,
    212        sex=sex,
    213        year_of_birth=year_of_birth,
    214    )
    215
    216    reality = mary.say_hello()
    217    # assert reality == None
    218    assert reality == my_expectation
    219
    220
    221# Exceptions seen
    

    the terminal is my friend, and shows AttributeError

    AttributeError: module 'src.person' has no attribute 'Person'
    

    because there is nothing with that name in the person.py file in the src folder.


GREEN: make it pass


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

  • I add the name to person.py

    1Person
    2
    3
    4def say_hello(
    5    first_name, last_name, year_of_birth,
    6):
    

    the terminal is my friend, and shows NameError

    NameError: name 'Person' is not defined
    
  • I point it to None to define it

    1# Person
    2Person = None
    3
    4
    5def say_hello(
    6    first_name, last_name, year_of_birth,
    7):
    

    the terminal is my friend, and shows TypeError

    TypeError: 'NoneType' object is not callable
    

    because I cannot call None like a function.

  • I change Person to a function

    1# Person
    2# Person = None
    3def Person():
    4    return None
    5
    6
    7def say_hello(
    8    first_name, last_name, year_of_birth,
    9):
    

    the terminal is my friend, and shows TypeError

    TypeError: Person() got
               an unexpected keyword argument 'first_name'
    

    because the Person function got called with a name (first_name) that is not in the parentheses of its definition.

  • I add first_name to the parentheses

     1# Person
     2# Person = None
     3# def Person():
     4def Person(first_name):
     5    return None
     6
     7
     8def say_hello(
     9    first_name, last_name, year_of_birth,
    10):
    

    the terminal is my friend, and shows TypeError

    TypeError: Person() got
               an unexpected keyword argument 'last_name'.
               Did you mean 'first_name'?
    

    because the Person function got called with a name (last_name) that is not in the parentheses of its definition.

  • I add last_name to the parentheses

     1# Person
     2# Person = None
     3# def Person():
     4# def Person(first_name):
     5def Person(first_name, last_name):
     6    return None
     7
     8
     9def say_hello(
    10    first_name, last_name, year_of_birth,
    11):
    

    the terminal is my friend, and shows TypeError

    TypeError: Person() got
               an unexpected keyword argument 'sex'
    

    because the Person function got called with a name (sex) that is not in the parentheses of its definition.

  • I add sex to the parentheses

     1# Person
     2# Person = None
     3# def Person():
     4# def Person(first_name):
     5# def Person(first_name, last_name):
     6def Person(first_name, last_name, sex):
     7    return None
     8
     9
    10def say_hello(
    11    first_name, last_name, year_of_birth,
    12):
    

    the terminal is my friend, and shows TypeError

    TypeError: Person() got
               an unexpected keyword argument 'year_of_birth'
    

    because the Person function got called with a name (year_of_birth) that is not in the parentheses of its definition.

  • I add year_of_birth to the parentheses

     1# Person
     2# Person = None
     3# def Person():
     4# def Person(first_name):
     5# def Person(first_name, last_name):
     6# def Person(first_name, last_name, sex):
     7def Person(
     8    first_name, last_name,
     9    sex, year_of_birth,
    10):
    11    return None
    12
    13
    14def say_hello(
    15    first_name, last_name, year_of_birth,
    16):
    

    the terminal is my friend, and shows AttributeError

    AttributeError: 'NoneType' object
                    has no attribute 'say_hello'
    

    because the function I just made returns None

    • given

      first_name = 'mary'
      last_name = 'public'
      sex = 'F'
      year_of_birth = 2000
      
      mary = Person(
          first_name=first_name,
          last_name=last_name,
          sex=sex,
          year_of_birth=year_of_birth,
      )
      └── Person(
              first_name='mary',
              last_name='public',
              sex='F',
              year_of_birth=2000,
          )
          └── return None
      
      reality = mary.say_hello()
      reality = None.say_hello()
      

    which raises AttributeError since None does not have anything named say_hello in it.

  • I change Person to a class

     1# Person
     2# Person = None
     3# def Person():
     4# def Person(first_name):
     5# def Person(first_name, last_name):
     6# def Person(first_name, last_name, sex):
     7# def Person(
     8class Person(
     9    first_name, last_name,
    10    sex, year_of_birth,
    11):
    12    return None
    13
    14
    15def say_hello(
    16    first_name, last_name, year_of_birth,
    17):
    

    the terminal is my friend, and shows SyntaxError

    SyntaxError: 'return' outside function
    
  • I add SyntaxError to the list of Exceptions seen, in test_person.py

    221# Exceptions seen
    222# AssertionError
    223# NameError
    224# TypeError
    225# AttributeError
    226# SyntaxError
    
  • I change the return statement to the pass keyword, in person.py

     1# Person
     2# Person = None
     3# def Person():
     4# def Person(first_name):
     5# def Person(first_name, last_name):
     6# def Person(first_name, last_name, sex):
     7# def Person(
     8class Person(
     9    first_name, last_name,
    10    sex, year_of_birth,
    11):
    12    # return None
    13    pass
    14
    15
    16def say_hello(
    17    first_name, last_name, year_of_birth,
    18):
    

    the terminal is my friend, and shows NameError

    NameError: name 'first_name' is not defined
    

    because the only definitions for first_name are in the say_hello and factory functions in person.py.

  • I add the constructor method to handle the inputs

     1# Person
     2# Person = None
     3# def Person():
     4# def Person(first_name):
     5# def Person(first_name, last_name):
     6# def Person(first_name, last_name, sex):
     7# def Person(
     8# class Person(
     9#     first_name, last_name,
    10#     sex, year_of_birth,
    11# ):
    12class Person:
    13
    14    def __init__(
    15        first_name, last_name,
    16        sex, year_of_birth,
    17    ):
    18        # return None
    19        pass
    20
    21
    22def say_hello(
    23    first_name, last_name, year_of_birth,
    24):
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.__init__() got
               multiple values for argument 'first_name'
    

    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 constructor method

    12class Person:
    13
    14    def __init__(
    15        # first_name, last_name,
    16        self, first_name, last_name,
    17        sex, year_of_birth,
    18    ):
    19        # return None
    20        pass
    21
    22
    23def say_hello(
    24    first_name, last_name, year_of_birth,
    25):
    

    the terminal is my friend, and shows AttributeError

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

    better, I can add an attribute to a class.

  • I add the name to the Person class

    12class Person:
    13
    14    say_hello
    15
    16    def __init__(
    17        # first_name, last_name,
    18        self, first_name, last_name,
    19        sex, year_of_birth,
    20    ):
    

    the terminal is my friend, and shows NameError

    NameError: name 'say_hello' is not defined
    
  • I point say_hello to None to define it

    12class Person:
    13
    14    # say_hello
    15    say_hello = None
    16
    17    def __init__(
    18        # first_name, last_name,
    19        self, first_name, last_name,
    20        sex, year_of_birth,
    21    ):
    

    the terminal is my friend, and shows TypeError

    TypeError: 'NoneType' object is not callable
    

    because I cannot call None like a function.

  • I change it to a method

    12class Person:
    13
    14    # say_hello
    15    # say_hello = None
    16
    17    def __init__(
    18        # first_name, last_name,
    19        self, first_name, last_name,
    20        sex, year_of_birth,
    21    ):
    22        # return None
    23        pass
    24
    25    def say_hello():
    26        return None
    27
    28
    29def say_hello(
    30    first_name, last_name, year_of_birth,
    31):
    

    the terminal shows TypeError

    TypeError: Person.say_hello() takes
               0 positional arguments but 1 was given
    
  • I add a name to the parentheses

    25    # def say_hello():
    26    def say_hello(argument):
    27        return None
    28
    29
    30def say_hello(
    31    first_name, last_name, year_of_birth,
    32):
    

    the terminal is my friend, and shows AssertionError

    AssertionError: assert None
        == 'Hello, my name is mary public and I am 26.'
    
  • I copy and paste the string from the terminal to use as the return statement

    25    # def say_hello():
    26    def say_hello(argument):
    27        # return None
    28        return 'Hello, my name is mary public and I am 26.'
    29
    30
    31def say_hello(
    32    first_name, last_name, year_of_birth,
    33):
    

    the test passes.


REFACTOR: make it better


  • I remove the commented lines from test_mary in test_person.py

    178def test_mary():
    179    first_name = 'mary'
    180    last_name = 'public'
    181    sex = 'F'
    182    year_of_birth = 2000
    183
    184    reality = src.person.factory(
    185        first_name=first_name,
    186        last_name=last_name,
    187        sex=sex,
    188        year_of_birth=year_of_birth,
    189    )
    190    my_expectation = (
    191        f'{first_name}, {last_name},'
    192        f' {sex}, {year_of_birth}'
    193    )
    194    assert reality == my_expectation
    195
    196    reality = src.person.say_hello(
    197        first_name=first_name,
    198        last_name=last_name,
    199        year_of_birth=year_of_birth,
    200    )
    201    my_expectation = (
    202        f'Hello, my name is {first_name}'
    203        f' {last_name} and I am'
    204        f' {2026-year_of_birth}.'
    205    )
    206    assert reality == my_expectation
    207
    208    mary = src.person.Person(
    209        first_name=first_name,
    210        last_name=last_name,
    211        sex=sex,
    212        year_of_birth=year_of_birth,
    213    )
    214
    215    reality = mary.say_hello()
    216    assert reality == my_expectation
    217
    218
    219# Exceptions seen
    220# AssertionError
    221# NameError
    222# TypeError
    223# AttributeError
    224# SyntaxError
    
  • I change john in test_john to be the result of a call to the Person class of the person module in the src folder

    154    reality = src.person.say_hello(
    155        first_name=first_name,
    156        last_name=last_name,
    157        year_of_birth=year_of_birth,
    158    )
    159    my_expectation = (
    160        f'Hello, my name is {first_name}'
    161        f' {last_name} and I am'
    162        f' {2026-year_of_birth}.'
    163    )
    164    assert reality == my_expectation
    165
    166    # john = Person(
    167    john = src.person.Person(
    168        first_name=first_name,
    169        last_name=last_name,
    170        sex=sex,
    171        year_of_birth=year_of_birth,
    172    )
    173
    174    reality = john.say_hello()
    175    # assert reality == None
    176    assert reality == my_expectation
    177
    178
    179def test_mary():
    

    the terminal is my friend, and shows AssertionError

    AssertionError: assert 'Hello, my name ... and I am 26.'
                        == 'Hello, my name ...and I am 446.'
    
  • I change the return statement of the say_hello method to return the input, in person.py

    25    # def say_hello():
    26    def say_hello(argument):
    27        # return None
    28        # return 'Hello, my name is mary public and I am 26.'
    29        return argument
    30
    31
    32def say_hello(
    33    first_name, last_name, year_of_birth,
    34):
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        assert <src.person.Person object at 0xffffb012cd34>
            == 'Hello, my name is mary public and I am 26.'
    

    because argument is an instance (a copy) of the Person class.

  • I change the return statement to use class attributes in an f-string

    25    # def say_hello():
    26    def say_hello(argument):
    27        # return None
    28        # return 'Hello, my name is mary public and I am 26.'
    29        # return argument
    30        return (
    31            f'Hello, my name is {argument.first_name}'
    32            f' {argument.last_name} and I am'
    33            f' {2026-argument.year_of_birth}.'
    34        )
    35
    36
    37def say_hello(
    38    first_name, last_name, year_of_birth,
    39):
    

    the terminal is my friend, and shows AttributeError

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

    because I have not defined a class attribute named first_name.

  • I add self.first_name to the __init__ constructor method

    12class Person:
    13
    14    # say_hello
    15    # say_hello = None
    16
    17    def __init__(
    18        # first_name, last_name,
    19        self, first_name, last_name,
    20        sex, year_of_birth,
    21    ):
    22        # return None
    23        # pass
    24        self.first_name = first_name
    25
    26    # def say_hello():
    

    the terminal is my friend, and shows AttributeError

    AttributeError: 'Person' object
                    has no attribute 'last_name'.
                    Did you mean: 'first_name'?
    

    because I have not defined a class attribute named last_name.

  • I add self.last_name to the __init__ constructor method

    17    def __init__(
    18        # first_name, last_name,
    19        self, first_name, last_name,
    20        sex, year_of_birth,
    21    ):
    22        # return None
    23        # pass
    24        self.first_name = first_name
    25        self.last_name = last_name
    26
    27    # def say_hello():
    

    the terminal is my friend, and shows AttributeError

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

    because I have not defined a class attribute named year_of_birth.

  • I add self.year_of_birth to the __init__ constructor method

    17    def __init__(
    18        # first_name, last_name,
    19        self, first_name, last_name,
    20        sex, year_of_birth,
    21    ):
    22        # return None
    23        # pass
    24        self.first_name = first_name
    25        self.last_name = last_name
    26        self.year_of_birth = year_of_birth
    27
    28    # def say_hello():
    

    the test passes.

  • I change argument to self in the say_hello method to follow Python convention

    28    # def say_hello():
    29    # def say_hello(argument):
    30    def say_hello(self):
    31        # return None
    32        # return 'Hello, my name is mary public and I am 26.'
    33        # return argument
    34        return (
    35            # f'Hello, my name is {argument.first_name}'
    36            # f' {argument.last_name} and I am'
    37            # f' {2026-argument.year_of_birth}.'
    38            f'Hello, my name is {self.first_name}'
    39            f' {self.last_name} and I am'
    40            f' {2026-self.year_of_birth}.'
    41        )
    42
    43
    44def say_hello(
    45    first_name, last_name, year_of_birth,
    46):
    

    the test is still green because a method of an instance takes the instance of the class (self) it belongs to as the first argument which means

    instance = Person()
    instance.say_hello() == Person().say_hello()
    instance.say_hello() == instance.say_hello(Person())
    instance.say_hello() == instance.say_hello(instance)
    

    I do not need to pass the instance as input to the say_hello method since it is self.

  • I remove the commented lines from test_john in test_person.py

    136def test_john():
    137    first_name = 'john'
    138    last_name = 'smith'
    139    sex = 'M'
    140    year_of_birth = 1580
    141
    142    reality = src.person.factory(
    143        first_name=first_name,
    144        last_name=last_name,
    145        sex=sex,
    146        year_of_birth=year_of_birth,
    147    )
    148    my_expectation = (
    149        f'{first_name}, {last_name},'
    150        f' {sex}, {year_of_birth}'
    151    )
    152    assert reality == my_expectation
    153
    154    reality = src.person.say_hello(
    155        first_name=first_name,
    156        last_name=last_name,
    157        year_of_birth=year_of_birth,
    158    )
    159    my_expectation = (
    160        f'Hello, my name is {first_name}'
    161        f' {last_name} and I am'
    162        f' {2026-year_of_birth}.'
    163    )
    164    assert reality == my_expectation
    165
    166    john = src.person.Person(
    167        first_name=first_name,
    168        last_name=last_name,
    169        sex=sex,
    170        year_of_birth=year_of_birth,
    171    )
    172
    173    reality = john.say_hello()
    174    assert reality == my_expectation
    175
    176
    177def test_mary():
    
  • I change jane in test_jane to be the result of a call to the Person class of the person module in the src folder

    107    reality = src.person.say_hello(
    108        first_name=first_name,
    109        last_name=last_name,
    110        year_of_birth=year_of_birth,
    111    )
    112    my_expectation = (
    113        f'Hello, my name is {first_name}'
    114        f' {last_name} and I am'
    115        f' {2026-year_of_birth}.'
    116    )
    117    assert reality == my_expectation
    118
    119    # jane = Person(
    120    jane = src.person.Person(
    121        first_name=first_name,
    122        last_name=last_name,
    123        sex=sex,
    124        year_of_birth=year_of_birth,
    125    )
    126
    127    # reality = Person.say_hello(
    128    reality = jane.say_hello(
    129        # person=jane,
    130        # first_name=jane.first_name,
    131        # last_name=jane.last_name,
    132        # year_of_birth=jane.year_of_birth,
    133    )
    134    assert reality == my_expectation
    135
    136
    137def test_john():
    

    the test is still green.

  • I remove the commented lines from test_jane

     89def test_jane():
     90    first_name = 'jane'
     91    last_name = 'doe'
     92    sex = 'F'
     93    year_of_birth = 1991
     94
     95    reality = src.person.factory(
     96        first_name=first_name,
     97        last_name=last_name,
     98        sex=sex,
     99        year_of_birth=year_of_birth,
    100    )
    101    my_expectation = (
    102        f'{first_name}, {last_name},'
    103        f' {sex}, {year_of_birth}'
    104    )
    105    assert reality == my_expectation
    106
    107    reality = src.person.say_hello(
    108        first_name=first_name,
    109        last_name=last_name,
    110        year_of_birth=year_of_birth,
    111    )
    112    my_expectation = (
    113        f'Hello, my name is {first_name}'
    114        f' {last_name} and I am'
    115        f' {2026-year_of_birth}.'
    116    )
    117    assert reality == my_expectation
    118
    119    jane = src.person.Person(
    120        first_name=first_name,
    121        last_name=last_name,
    122        sex=sex,
    123        year_of_birth=year_of_birth,
    124    )
    125
    126    reality = jane.say_hello()
    127    assert reality == my_expectation
    128
    129
    130def test_john():
    
  • I change joe in test_joe to be the result of a call to the Person class of the person module in the src folder

    59    reality = src.person.say_hello(
    60        first_name=first_name,
    61        last_name=last_name,
    62        year_of_birth=year_of_birth,
    63    )
    64    my_expectation = (
    65        f'Hello, my name is {first_name}'
    66        f' {last_name} and I am'
    67        f' {2026-year_of_birth}.'
    68    )
    69    assert reality == my_expectation
    70
    71    # joe = Person(
    72    joe = src.person.Person(
    73        first_name=first_name,
    74        last_name=last_name,
    75        sex=sex,
    76        year_of_birth=year_of_birth,
    77    )
    78
    79    # reality = src.person.say_hello(
    80    # reality = Person.say_hello(
    81    reality = joe.say_hello(
    82        # person=joe,
    83        # first_name=joe.first_name,
    84        # last_name=joe.last_name,
    85        # year_of_birth=joe.year_of_birth,
    86    )
    87    assert reality == my_expectation
    88
    89
    90def test_jane():
    

    the test is still green.

  • I remove the commented lines from test_joe

    41def test_joe():
    42    first_name = 'joe'
    43    last_name = 'blow'
    44    sex = 'M'
    45    year_of_birth = 1996
    46
    47    reality = src.person.factory(
    48        first_name=first_name,
    49        last_name=last_name,
    50        sex=sex,
    51        year_of_birth=year_of_birth,
    52    )
    53    my_expectation = (
    54        f'{first_name}, {last_name},'
    55        f' {sex}, {year_of_birth}'
    56    )
    57    assert reality == my_expectation
    58
    59    reality = src.person.say_hello(
    60        first_name=first_name,
    61        last_name=last_name,
    62        year_of_birth=year_of_birth,
    63    )
    64    my_expectation = (
    65        f'Hello, my name is {first_name}'
    66        f' {last_name} and I am'
    67        f' {2026-year_of_birth}.'
    68    )
    69    assert reality == my_expectation
    70
    71    joe = src.person.Person(
    72        first_name=first_name,
    73        last_name=last_name,
    74        sex=sex,
    75        year_of_birth=year_of_birth,
    76    )
    77
    78    reality = joe.say_hello()
    79    assert reality == my_expectation
    80
    81
    82def test_jane():
    
  • I remove the Person class from test_person.py

    import src.person
    
    
    def test_joe():
    

    all the tests are still green because the calls that were made to the Person class that was in test_person.py are now made to the Person class in person.py in the src folder. When src.person.Person is called with input, Python follows this path

    src.person.Person
    src
    └── person.py
        └── class Person:
            └── def __init__(
                    self, first_name, last_name,
                    sex, year_of_birth,
                ):
                ├── self.first_name = first_name
                ├── self.last_name = last_name
                └── self.year_of_birth = year_of_birth
    
  • I add a git commit message in the other terminal

    git commit -am \
    'move Person class to person.py'
    

    the terminal shows a summary of the changes then goes back to the command line.

I can write solutions in a different module from the tests.


test_dir_person_class

Python has the dir built-in function which shows the attributes and methods of the object it is given in parentheses. It allows me to see what makes up an object without looking at the code or reading the documentation. I can then run tests to see what each thing does.


RED: make it fail


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

  • I add a new test with the dir built-in function in test_person.py

    157    mary = src.person.Person(
    158        first_name=first_name,
    159        last_name=last_name,
    160        sex=sex,
    161        year_of_birth=year_of_birth,
    162    )
    163
    164    reality = mary.say_hello()
    165    assert reality == my_expectation
    166
    167
    168def test_dir_person_class():
    169    reality = dir(src.person.Person)
    170    my_expectation = None
    171    assert reality == my_expectation
    172
    173
    174# Exceptions seen
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        assert ['__class__', '__delattr__', '__dict__',
                '__dir__', '__doc__', '__eq__', ...]
            == None
    

    because dir returned a list (anything in square brackets [ ]) and my_expectation is None.


GREEN: make it pass


  • I copy (ctrl/command+c) the values from the terminal and paste (ctrl/command+v) them as my_expectation

    168def test_dir_person_class():
    169    reality = dir(src.person.Person)
    170    # my_expectation = None
    171    my_expectation = [
    172        '__class__', '__delattr__', '__dict__',
    173        '__dir__', '__doc__', '__eq__', ...
    174    ]
    175    assert reality == my_expectation
    176
    177
    178# Exceptions seen
    

    the terminal is my friend, and shows AssertionError

    E         AssertionError:
                  assert ['__class__',...'__eq__', ...]
                      == ['__class__',...'__eq__', ...]
    E
    E         At index 6 diff: '__firstlineno__' != Ellipsis
    E         Left contains 23 more items,
                  first extra item: '__format__'
    E         Use -v to get more diff
    
  • I click in the terminal where the tests are running then press v on the keyboard for pytest-watcher to show me more of the difference between reality and my_expectation and it shows AssertionError

    E         ...Full output truncated (31 lines hidden),
                 use '-vv' to show
    
  • I press w on the keyboard in the terminal where the tests are running, to show the menu for pytest-watcher and it shows

    [pytest-watcher]
    Current runner args: [-v]
    
    Controls:
    > Enter : Invoke test runner
    > r     : reset all runner args
    > c     : change runner args
    > f     : run only failed tests (--lf)
    > p     : drop to pdb on fail (--pdb)
    > v     : increase verbosity (-v)
    > e     : Erase terminal screen
    > q     : quit pytest-watcher
    
  • I press c on the keyboard to change runner args, and the terminal shows

    [pytest-watcher]
    Current runner args: []
    
    Controls:
    > Enter : Invoke test runner
    > r     : reset all runner args
    > c     : change runner args
    > f     : run only failed tests (--lf)
    > p     : drop to pdb on fail (--pdb)
    > v     : increase verbosity (-v)
    > e     : Erase terminal screen
    > q     : quit pytest-watcher
    
    Enter new runner args: -vv
    
  • I type -vv then press enter to show the full difference, and the terminal shows AssertionError with the full list.

  • I copy (ctrl/command+c) the values from the terminal and paste (ctrl/command+v) them as my_expectation

    168def test_dir_person_class():
    169    reality = dir(src.person.Person)
    170    my_expectation = [
    171        '__class__',
    172        '__delattr__',
    173        '__dict__',
    174        '__dir__',
    175        '__doc__',
    176        '__eq__',
    177        '__firstlineno__',
    178        '__format__',
    179        '__ge__',
    180        '__getattribute__',
    181        '__getstate__',
    182        '__gt__',
    183        '__hash__',
    184        '__init__',
    185        '__init_subclass__',
    186        '__le__',
    187        '__lt__',
    188        '__module__',
    189        '__ne__',
    190        '__new__',
    191        '__reduce__',
    192        '__reduce_ex__',
    193        '__repr__',
    194        '__setattr__',
    195        '__sizeof__',
    196        '__static_attributes__',
    197        '__str__',
    198        '__subclasshook__',
    199        '__weakref__',
    200        'say_hello'
    201    ]
    202    assert reality == my_expectation
    203
    204
    205# Exceptions seen
    
  • I add a git commit message in the other terminal

    git commit -am \
    'add test_dir_person_class'
    

Caution

Your list of attributes and methods can be different because of your Python version.


test_dir_person_instance

RED: make it fail


I add a test to see the difference between the attributes and methods of an instance and the actual class

197          '__str__',
198          '__subclasshook__',
199          '__weakref__',
200          'say_hello'
201      ]
202      assert reality == my_expectation
203
204
205  def test_dir_person_instance():
206      an_instance_of_person = src.person.Person(
207          first_name='first_name',
208          last_name='last_name',
209          sex='M',
210          year_of_birth=2026,
211      )
212
213      reality = dir(an_instance_of_person)
214      my_expectation = [
215          '__class__',
216          '__delattr__',
217          '__dict__',
218          '__dir__',
219          '__doc__',
220          '__eq__',
221          '__firstlineno__',
222          '__format__',
223          '__ge__',
224          '__getattribute__',
225          '__getstate__',
226          '__gt__',
227          '__hash__',
228          '__init__',
229          '__init_subclass__',
230          '__le__',
231          '__lt__',
232          '__module__',
233          '__ne__',
234          '__new__',
235          '__reduce__',
236          '__reduce_ex__',
237          '__repr__',
238          '__setattr__',
239          '__sizeof__',
240          '__static_attributes__',
241          '__str__',
242          '__subclasshook__',
243          '__weakref__',
244          'say_hello'
245      ]
246      assert reality == my_expectation
247
248
249  # Exceptions seen

the terminal is my friend, and shows AssertionError

AssertionError:
    assert [
        '__class__', '__delattr__', '__dict__', '__dir__',
        '__doc__', '__eq__', '__firstlineno__', '__format__',
        '__ge__', '__getattribute__', '__getstate__', '__gt__',
        '__hash__', '__init__', '__init_subclass__', '__le__',
        '__lt__', '__module__', '__ne__', '__new__', '__reduce__',
        '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
        '__static_attributes__', '__str__', '__subclasshook__',
        '__weakref__',
        'first_name', 'last_name', 'say_hello', 'year_of_birth'
    ]
 == [
        '__class__', '__delattr__', '__dict__', '__dir__',
        '__doc__', '__eq__', '__firstlineno__', '__format__',
        '__ge__', '__getattribute__', '__getstate__', '__gt__',
        '__hash__', '__init__', '__init_subclass__', '__le__',
        '__lt__', '__module__', '__ne__', '__new__', '__reduce__',
        '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
        '__static_attributes__', '__str__', '__subclasshook__',
        '__weakref__', 'say_hello'
    ]

because first_name, last_name and year_of_birth are missing. Why is there no sex?


GREEN: make it pass


I add the missing attributes to my_expectation

205def test_dir_person_instance():
206    an_instance_of_person = src.person.Person(
207        first_name='first_name',
208        last_name='last_name',
209        sex='M',
210        year_of_birth=2026,
211    )
212
213    reality = dir(an_instance_of_person)
214    my_expectation = [
215        '__class__',
216        '__delattr__',
217        '__dict__',
218        '__dir__',
219        '__doc__',
220        '__eq__',
221        '__firstlineno__',
222        '__format__',
223        '__ge__',
224        '__getattribute__',
225        '__getstate__',
226        '__gt__',
227        '__hash__',
228        '__init__',
229        '__init_subclass__',
230        '__le__',
231        '__lt__',
232        '__module__',
233        '__ne__',
234        '__new__',
235        '__reduce__',
236        '__reduce_ex__',
237        '__repr__',
238        '__setattr__',
239        '__sizeof__',
240        '__static_attributes__',
241        '__str__',
242        '__subclasshook__',
243        '__weakref__',
244        'first_name',
245        'last_name',
246        'say_hello',
247        'year_of_birth',
248    ]
249    assert reality == my_expectation
250
251
252# Exceptions seen

the test passes.


REFACTOR: make it better


  • I add sex to the list

    244        'first_name',
    245        'last_name',
    246        'say_hello',
    247        'sex',
    248        'year_of_birth',
    249    ]
    250    assert reality == my_expectation
    251
    252
    253# Exceptions seen
    254# AssertionError
    255# NameError
    256# TypeError
    257# AttributeError
    258# SyntaxError
    

    the terminal is my friend, and shows AssertionError

      AssertionError:
          assert [
              '__class__', '__delattr__', '__dict__', '__dir__',
              '__doc__', '__eq__', '__firstlineno__', '__format__',
              '__ge__', '__getattribute__', '__getstate__', '__gt__',
              '__hash__', '__init__', '__init_subclass__', '__le__',
              '__lt__', '__module__', '__ne__', '__new__', '__reduce__',
              '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
              '__static_attributes__', '__str__', '__subclasshook__',
              '__weakref__',
              'first_name', 'last_name', 'say_hello', 'year_of_birth'
          ]
       == [
              '__class__', '__delattr__', '__dict__', '__dir__',
              '__doc__', '__eq__', '__firstlineno__', '__format__',
              '__ge__', '__getattribute__', '__getstate__', '__gt__',
              '__hash__', '__init__', '__init_subclass__', '__le__',
              '__lt__', '__module__', '__ne__', '__new__', '__reduce__',
              '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
              '__static_attributes__', '__str__', '__subclasshook__',
              '__weakref__', 'first_name', 'last_name', 'say_hello',
              'sex', 'year_of_birth'
          ]
    

    the sex attribute is not defined anywhere in the Person class.

  • I add self.sex to the __init__ method of the Person class in person.py

     1# Person
     2# Person = None
     3# def Person():
     4# def Person(first_name):
     5# def Person(first_name, last_name):
     6# def Person(first_name, last_name, sex):
     7# def Person(
     8# class Person(
     9#     first_name, last_name,
    10#     sex, year_of_birth,
    11# ):
    12class Person:
    13
    14    # say_hello
    15    # say_hello = None
    16
    17    def __init__(
    18        # first_name, last_name,
    19        self, first_name, last_name,
    20        sex, year_of_birth,
    21    ):
    22        # return None
    23        # pass
    24        self.first_name = first_name
    25        self.last_name = last_name
    26        self.year_of_birth = year_of_birth
    27        self.sex = sex
    28
    29    # def say_hello():
    30    # def say_hello(argument):
    31    def say_hello(self):
    32        # return None
    33        # return 'Hello, my name is mary public and I am 26.'
    34        # return argument
    35        return (
    36            # f'Hello, my name is {argument.first_name}'
    37            # f' {argument.last_name} and I am'
    38            # f' {2026-argument.year_of_birth}.'
    39            f'Hello, my name is {self.first_name}'
    40            f' {self.last_name} and I am'
    41            f' {2026-self.year_of_birth}.'
    42        )
    43
    44
    45def say_hello(
    46    first_name, last_name, year_of_birth,
    47):
    

    the test passes.

  • I remove the commented lines

     1class Person:
     2
     3    def __init__(
     4        self, first_name, last_name,
     5        sex, year_of_birth,
     6    ):
     7        self.first_name = first_name
     8        self.last_name = last_name
     9        self.year_of_birth = year_of_birth
    10        self.sex = sex
    11
    12    def say_hello(self):
    13        return (
    14            f'Hello, my name is {self.first_name}'
    15            f' {self.last_name} and I am'
    16            f' {2026-self.year_of_birth}.'
    17        )
    18
    19
    20def say_hello(
    21    first_name, last_name, year_of_birth,
    22):
    23    return (
    24        f'Hello, my name is {first_name}'
    25        f' {last_name} and I am'
    26        f' {2026-year_of_birth}.'
    27    )
    28
    29
    30def factory(
    31        first_name, last_name,
    32        sex, year_of_birth,
    33    ):
    34    return (
    35        f'{first_name}, {last_name},'
    36        f' {sex}, {year_of_birth}'
    37    )
    
  • I add a git commit message in the other terminal

    git commit -am \
    'add test_dir_person_instance'
    

close the project

  • I close person.py and test_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 ran tests to write a class that makes a person when given first_name, last_name, sex and year_of_birth and has a method so I do not have to pass the same values every time I want to do something with a person.

  • I saw the following Exceptions

  • My tests have a problem, each test is now the same three tests. There has to be a way that I can use one test for all the people.


code from the chapter

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


what is next?

Would you like to know where the extra attributes and methods of the Person class came from?


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.