how to make a person with an object


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

I think of objects 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 assert_equal(left, right):
5    assert left == right
person/tests/test_person.py
 8def assert_person_can_say_hello(
 9        first_name, last_name,
10        sex, year_of_birth,
11    ):
12    assert_equal(
13        src.person.Person(
14            first_name=first_name,
15            last_name=last_name,
16            sex=sex,
17            year_of_birth=year_of_birth,
18        ).say_hello(),
19        (
20            f'Hello, my name is {first_name}'
21            f' {last_name} and I am'
22            f' {2026-year_of_birth}.'
23        )
24    )
person/tests/test_person.py
27def assert_say_hello_works(
28        first_name, last_name,
29        year_of_birth,
30    ):
31    assert_equal(
32        src.person.say_hello(
33            first_name=first_name,
34            last_name=last_name,
35            year_of_birth=year_of_birth
36        ),
37        (
38            f'Hello, my name is {first_name}'
39            f' {last_name} and I am'
40            f' {2026-year_of_birth}.'
41        )
42    )
person/tests/test_person.py
45def assert_person_factory_works(
46        first_name, last_name,
47        sex, year_of_birth
48    ):
49    assert_equal(
50        src.person.person(
51            first_name=first_name,
52            last_name=last_name,
53            sex=sex,
54            year_of_birth=year_of_birth,
55        ),
56        (
57            f'{first_name}, {last_name},'
58            f' {sex}, {year_of_birth}'
59        )
60    )
person/tests/test_person.py
63def test_joe():
64    first_name = 'joe'
65    last_name = 'blow'
66    sex = 'M'
67    year_of_birth = 1996
68
69    assert_person_factory_works(
70        first_name=first_name,
71        last_name=last_name,
72        sex=sex,
73        year_of_birth=year_of_birth
74    )
75
76    assert_say_hello_works(
77        first_name=first_name,
78        last_name=last_name,
79        year_of_birth=year_of_birth,
80    )
81
82    assert_person_can_say_hello(
83        first_name=first_name,
84        last_name=last_name,
85        sex=sex,
86        year_of_birth=year_of_birth
87    )
person/tests/test_person.py
 90def test_jane():
 91    first_name = 'jane'
 92    last_name = 'doe'
 93    sex = 'F'
 94    year_of_birth = 1991
 95
 96    assert_person_factory_works(
 97        first_name=first_name,
 98        last_name=last_name,
 99        sex=sex,
100        year_of_birth=year_of_birth,
101    )
102
103    assert_say_hello_works(
104        first_name=first_name,
105        last_name=last_name,
106        year_of_birth=year_of_birth,
107    )
108
109    assert_person_can_say_hello(
110        first_name=first_name,
111        last_name=last_name,
112        sex=sex,
113        year_of_birth=year_of_birth,
114    )
person/tests/test_person.py
117def test_john():
118    first_name = 'john'
119    last_name = 'smith'
120    sex = 'M'
121    year_of_birth = 1580
122
123    assert_person_factory_works(
124        first_name=first_name,
125        last_name=last_name,
126        sex=sex,
127        year_of_birth=year_of_birth,
128    )
129
130    assert_say_hello_works(
131        first_name=first_name,
132        last_name=last_name,
133        year_of_birth=year_of_birth,
134    )
135
136    assert_person_can_say_hello(
137        first_name=first_name,
138        last_name=last_name,
139        sex=sex,
140        year_of_birth=year_of_birth,
141    )
person/tests/test_person.py
144def test_mary():
145    first_name = 'mary'
146    last_name = 'public'
147    sex = 'F'
148    year_of_birth = 2000
149
150    assert_person_factory_works(
151        first_name=first_name,
152        last_name=last_name,
153        sex=sex,
154        year_of_birth=year_of_birth,
155    )
156
157    assert_say_hello_works(
158        first_name=first_name,
159        last_name=last_name,
160        year_of_birth=year_of_birth,
161    )
162
163    assert_person_can_say_hello(
164        first_name=first_name,
165        last_name=last_name,
166        sex=sex,
167        year_of_birth=year_of_birth,
168    )
person/tests/test_person.py
171def test_dir_person_class():
172    assert_equal(
173        dir(src.person.Person),
174        [
175            '__class__', '__delattr__', '__dict__',
176            '__dir__', '__doc__', '__eq__',
177            '__firstlineno__', '__format__', '__ge__',
178            '__getattribute__', '__getstate__', '__gt__',
179            '__hash__', '__init__', '__init_subclass__',
180            '__le__', '__lt__', '__module__', '__ne__',
181            '__new__', '__reduce__', '__reduce_ex__',
182            '__repr__', '__setattr__', '__sizeof__',
183            '__static_attributes__', '__str__',
184            '__subclasshook__', '__weakref__', 'say_hello'
185        ]
186    )
person/tests/test_person.py
188def test_dir_person_instance():
189    assert_equal(
190        dir(
191            src.person.Person(
192                first_name='first_name',
193                last_name='last_name',
194                sex='M',
195                year_of_birth=2026,
196            )
197        ),
198        [
199            '__class__', '__delattr__', '__dict__',
200            '__dir__', '__doc__', '__eq__',
201            '__firstlineno__', '__format__', '__ge__',
202            '__getattribute__', '__getstate__', '__gt__',
203            '__hash__', '__init__', '__init_subclass__',
204            '__le__', '__lt__', '__module__', '__ne__',
205            '__new__', '__reduce__', '__reduce_ex__',
206            '__repr__', '__setattr__', '__sizeof__',
207            '__static_attributes__', '__str__',
208            '__subclasshook__', '__weakref__', 'first_name',
209            'last_name', 'say_hello', 'sex', 'year_of_birth',
210        ]
211    )
212
213
214# Exceptions seen
215# AssertionError
216# NameError
217# TypeError
218# AttributeError
219# 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 ====================
    

add 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 an object because it is attributes and methods that belong together.


RED: make it fail


I make an instance of an object to represent joe in test_joe in tests/test_person.py

38def test_joe():
39    first_name = 'joe'
40    last_name = 'blow'
41    sex = 'M'
42    year_of_birth = 1996
43
44    assert_person_factory_works(
45        first_name=first_name,
46        last_name=last_name,
47        sex=sex,
48        year_of_birth=year_of_birth
49    )
50
51    assert_say_hello_works(
52        first_name=first_name,
53        last_name=last_name,
54        year_of_birth=year_of_birth,
55    )
56
57    joe = Person(
58        first_name=first_name,
59        last_name=last_name,
60        sex=sex,
61        year_of_birth=year_of_birth,
62    )
63
64
65def test_jane():

the terminal is my friend, and shows NameError

NameError: name 'Person' is not defined

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


GREEN: make it pass



the constructor method

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

  • I add the 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 assert_say_hello_works(
    12        first_name, last_name,
    13        year_of_birth,
    14    ):
    

    the terminal is my friend, and shows TypeError

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

    I am violating the method signature when I call it in a way that is different from its definition.

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

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

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

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

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

    Python does not know which value to use for the first argument if I use its name and position.

  • 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
    
    • 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'?
      
    • self is for the instance of the class.

    • I am violating the method signature when I call it in a way that is different from its definition

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

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

    • this is the same as making the person 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
    

    the terminal is my friend, and shows TypeError

    TypeError:
        Person.__init__() got
        an unexpected keyword argument 'sex'
    
    • self is the instance of the class.

    • I am violating the method signature when I call it in a way that is different from its definition.

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

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

    • Still the same as making the person 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
    

    the terminal is my friend, and shows TypeError

    TypeError: Person.__init__() got
               an unexpected keyword argument 'year_of_birth'
    
    • self is the instance of the class.

    • I am violating the method signature when I call it in a way that is different from its definition.

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

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

    • Same as with the person function.

  • I add year_of_birth 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            sex, year_of_birth,
    15        ):
    16        return None
    

    the test passes.


REFACTOR: make it better


  • I remove the commented lines from the Person class

     4class Person:
     5
     6    def __init__(
     7            self, first_name, last_name,
     8            sex, year_of_birth,
     9        ):
    10        return None
    11
    12
    13def assert_say_hello_works(
    14        first_name, last_name,
    15        year_of_birth,
    16    ):
    
  • 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'
    

add say_hello method

I made a person say hi with a function, I can also do the same thing with an object 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

    47def test_joe():
    48    first_name = 'joe'
    49    last_name = 'blow'
    50    sex = 'M'
    51    year_of_birth = 1996
    52
    53    assert_person_factory_works(
    54        first_name=first_name,
    55        last_name=last_name,
    56        sex=sex,
    57        year_of_birth=year_of_birth
    58    )
    59
    60    assert_say_hello_works(
    61        first_name=first_name,
    62        last_name=last_name,
    63        year_of_birth=year_of_birth,
    64    )
    65
    66    joe = Person(
    67        first_name=first_name,
    68        last_name=last_name,
    69        sex=sex,
    70        year_of_birth=year_of_birth,
    71    )
    72
    73    reality = src.person.say_hello(
    74        first_name=joe.first_name,
    75        last_name=joe.last_name,
    76        year_of_birth=joe.year_of_birth,
    77    )
    78    my_expectation = (
    79        f'Hello, my name is {first_name}'
    80        f' {last_name} and I am'
    81        f' {2026-year_of_birth}.'
    82    )
    83    assert reality == my_expectation
    84
    85
    86def 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, so Python cannot reach the first_name attribute of joe.


GREEN: make it pass


  • I add self.first_name to the __init__ method of 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
    11        return None
    

    the terminal still shows AttributeError because all I have done is add a reference to the first_name, not defined it.

  • 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
    

    the terminal is my friend, and shows AttributeError

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

    because there is nothing named last_name in the Person class, so Python cannot reach the last_name attribute of joe.

  • 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
    

    the terminal is my friend, and shows AttributeError

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

    because there is nothing named year_of_birth in the Person class, so Python cannot reach the year_of_birth attribute of joe.

  • 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
    

    the test passes.

    ├── 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=first_name,
                last_name=last_name,
                sex=sex,
                year_of_birth=year_of_birth,
            )
            └── def __init__(
                    self, first_name, last_name,
                    sex, year_of_birth,
                ):
                ├── self.first_name = 'joe'
                ├── self.last_name = 'blow'
                ├── self.year_of_birth = 1996
                └── return None
    

    self is the instance of the class, joe in this case.

    src.person.say_hello(
        first_name=joe.first_name,
        last_name=joe.last_name,
        year_of_birth=joe.year_of_birth,
    )
    └── src/
        └── person/
            └── __init__.py
                └── def say_hello(
                        first_name, last_name, year_of_birth,
                    ):
                    ├── first_name    = 'joe'
                    ├── last_name     = 'blow'
                    ├── year_of_birth = 1996
                    └── return (
                            f'Hello, my name is {first_name}'
                            f' {last_name} and I am'
                            f' {2026-year_of_birth}.'
                        )
                        return 'Hello, my name is joe blow and I am 30.'
    

REFACTOR: make it better


  • I remove the commented line from 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
    16def assert_say_hello_works(
    17        first_name, last_name,
    18        year_of_birth,
    19    ):
    
  • I change the call to src.person.say_hello in test_joe to a call to the say_hello method of the Person class

    69    joe = Person(
    70        first_name=first_name,
    71        last_name=last_name,
    72        sex=sex,
    73        year_of_birth=year_of_birth,
    74    )
    75
    76    # reality = src.person.say_hello(
    77    reality = Person.say_hello(
    78        first_name=joe.first_name,
    79        last_name=joe.last_name,
    80        year_of_birth=joe.year_of_birth,
    81    )
    82    my_expectation = (
    83        f'Hello, my name is {first_name}'
    84        f' {last_name} and I am'
    85        f' {2026-year_of_birth}.'
    86    )
    87    assert reality == my_expectation
    88
    89
    90def 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 say_hello 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 assert_say_hello_works(
    20        first_name, last_name,
    21        year_of_birth,
    22    ):
    

    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
    

    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 (last_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
    

    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
    

    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 assert_say_hello_works(
    24        first_name, last_name,
    25        year_of_birth,
    26    ):
    

    the test passes.

    Person.say_hello(
        first_name=joe.first_name,
        last_name=joe.last_name,
        year_of_birth=joe.year_of_birth,
    )
    └── class Person:
        └── def say_hello(first_name, last_name, year_of_birth):
            ├── first_name    = 'joe'
            ├── last_name     = 'blow'
            ├── year_of_birth = 1996
            └── return 'Hello, my name is joe blow and I am 30.'
    
  • I add a call to the say_hello method of the Person class in test_jane

     97def test_jane():
     98    first_name = 'jane'
     99    last_name = 'doe'
    100    sex = 'F'
    101    year_of_birth = 1991
    102
    103    assert_person_factory_works(
    104        first_name=first_name,
    105        last_name=last_name,
    106        sex=sex,
    107        year_of_birth=year_of_birth,
    108    )
    109
    110    assert_say_hello_works(
    111        first_name=first_name,
    112        last_name=last_name,
    113        year_of_birth=year_of_birth,
    114    )
    115
    116    jane = 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 = Person.say_hello(
    124        first_name=jane.first_name,
    125        last_name=jane.last_name,
    126        year_of_birth=jane.year_of_birth,
    127    )
    128    my_expectation = (
    129        f'Hello, my name is {first_name}'
    130        f' {last_name} and I am'
    131        f' {2026-year_of_birth}.'
    132    )
    133    assert reality == my_expectation
    134
    135
    136def 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 like I did with the say_hello function

    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 assert_say_hello_works(
    29        first_name, last_name,
    30        year_of_birth,
    31    ):
    

    the test passes.

    ├── first_name = 'jane'
    ├── last_name = 'doe'
    ├── sex = 'F'
    ├── year_of_birth = 1991
    └── jane = Person(
            first_name=first_name,
            last_name=last_name,
            sex=sex,
            year_of_birth=year_of_birth,
        )
        └── Person.__init__(
                self,
                first_name=first_name,
                last_name=last_name,
                sex=sex,
                year_of_birth=year_of_birth,
            )
            └── def __init__(
                    self, first_name, last_name,
                    sex, year_of_birth,
                ):
                ├── self.first_name = 'jane'
                ├── self.last_name = 'doe'
                ├── self.year_of_birth = 1991
                └── return None
    
    Person.say_hello(
        first_name=jane.first_name,
        last_name=jane.last_name,
        year_of_birth=jane.year_of_birth,
    )
    └── def say_hello(first_name, last_name, year_of_birth):
        ├── first_name    = 'jane'
        ├── last_name     = 'doe'
        ├── year_of_birth = 1991
        └── return (
                f'Hello, my name is {first_name}'
                f' {last_name} and I am'
                f' {2026-year_of_birth}.'
            )
            return 'Hello, my name is jane doe and I am 35.'
    
  • I add an instance (copy) of the Person class to the call to the say_hello method from test_joe because the instance has the attributes I use in the method

     88    # reality = src.person.say_hello(
     89    reality = Person.say_hello(
     90        person=joe,
     91        first_name=joe.first_name,
     92        last_name=joe.last_name,
     93        year_of_birth=joe.year_of_birth,
     94    )
     95    my_expectation = (
     96        f'Hello, my name is {first_name}'
     97        f' {last_name} and I am'
     98        f' {2026-year_of_birth}.'
     99    )
    100    assert reality == my_expectation
    101
    102
    103def 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        )
    

    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 from test_jane

    132    reality = Person.say_hello(
    133        person=jane,
    134        first_name=jane.first_name,
    135        last_name=jane.last_name,
    136        year_of_birth=jane.year_of_birth,
    137    )
    138    my_expectation = (
    139        f'Hello, my name is {first_name}'
    140        f' {last_name} and I am'
    141        f' {2026-year_of_birth}.'
    142    )
    143    assert reality == my_expectation
    144
    145
    146def 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 assert_say_hello_works(
    35        first_name, last_name,
    36        year_of_birth,
    37    ):
    

    the tests are still green

    instance = Person(
        first_name=first_name,
        last_name=last_name,
        sex=sex,
        year_of_birth=year_of_birth,
    )
    └── Person.__init__(
            self,
            first_name=first_name,
            last_name=last_name,
            sex=sex,
            year_of_birth=year_of_birth,
        )
        └── 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
            └── return None
    
    Person.say_hello(
        person=instance,
        first_name=instance.first_name,
        last_name=instance.last_name,
        year_of_birth=instance.year_of_birth,
    )
    └── Person.say_hello(
            person=instance,
            first_name=instance.first_name,
            last_name=instance.last_name,
            year_of_birth=instance.year_of_birth,
        )
        └── def say_hello(
                person, first_name, last_name, year_of_birth,
            ):
            ├── person        = instance
            ├── first_name    = first_name
            ├── last_name     = last_name
            ├── year_of_birth = year_of_birth
            └── 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 {instance.first_name}'
                    f' {instance.last_name} and I am'
                    f' {2026-instance.year_of_birth}.'
                )
    
  • I remove the first_name, last_name and year_of_birth arguments from the call to the say_hello method from test_joe since they are repetitions of the object attributes

     94    # reality = src.person.say_hello(
     95    reality = Person.say_hello(
     96        person=joe,
     97        # first_name=joe.first_name,
     98        # last_name=joe.last_name,
     99        # year_of_birth=joe.year_of_birth,
    100    )
    101    my_expectation = (
    102        f'Hello, my name is {first_name}'
    103        f' {last_name} and I am'
    104        f' {2026-year_of_birth}.'
    105    )
    106    assert reality == my_expectation
    107
    108
    109def 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        )
    

    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 from test_jane 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 from test_jane

    136    reality = Person.say_hello(
    137        person=jane,
    138        # first_name=jane.first_name,
    139        # last_name=jane.last_name,
    140        # year_of_birth=jane.year_of_birth,
    141    )
    142    my_expectation = (
    143        f'Hello, my name is {first_name}'
    144        f' {last_name} and I am'
    145        f' {2026-year_of_birth}.'
    146    )
    147    assert reality == my_expectation
    148
    149
    150def 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 same object.

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

    136    # reality = Person.say_hello(
    137    reality = jane.say_hello(
    138        person=jane,
    139        # first_name=jane.first_name,
    140        # last_name=jane.last_name,
    141        # year_of_birth=jane.year_of_birth,
    142    )
    143    my_expectation = (
    144        f'Hello, my name is {first_name}'
    145        f' {last_name} and I am'
    146        f' {2026-year_of_birth}.'
    147    )
    148    assert reality == my_expectation
    149
    150
    151def 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 and I gave a value when I called the method.


what is the staticmethod decorator?

  • I can use the staticmethod decorator if I do not want to add self to the method definition when it does not use anything that belongs to the object, so I do not send more than what the method needs. I add @staticmethod<what is the staticmethod decorator?>` 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        ):
    

    the test passes.

  • I change the call to Person.say_hello from 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

     96    # reality = src.person.say_hello(
     97    # reality = Person.say_hello(
     98    reality = joe.say_hello(
     99        # person=joe,
    100        # first_name=joe.first_name,
    101        # last_name=joe.last_name,
    102        # year_of_birth=joe.year_of_birth,
    103    )
    104    my_expectation = (
    105        f'Hello, my name is {first_name}'
    106        f' {last_name} and I am'
    107        f' {2026-year_of_birth}.'
    108    )
    109    assert reality == my_expectation
    110
    111
    112def 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
    20    def 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        )
    

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

    instance = 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,
        )
        └── 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
            └── return None
    

    self is the instance of the class, instance in this case

    instance.say_hello()
    └── class Person:
        └── def say_hello(self, person=None):
            ├── self = instance
            └── return (
                    f'Hello, my name is {self.first_name}'
                    f' {self.last_name} and I am'
                    f' {2026-self.year_of_birth}.'
                )
                return (
                    f'Hello, my name is {instance.first_name}'
                    f' {instance.last_name} and I am'
                    f' {2026-instance.year_of_birth}.'
                )
    

    a simple way to think of instance.say_hello() is

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

    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 from test_jane because the say_hello method is in the Person class

    143    # reality = Person.say_hello(
    144    reality = jane.say_hello(
    145        # person=jane,
    146        # first_name=jane.first_name,
    147        # last_name=jane.last_name,
    148        # year_of_birth=jane.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
    158def test_john():
    

    the test is still green.

  • I add an assertion for the say_hello method to test_john

    158def test_john():
    159    first_name = 'john'
    160    last_name = 'smith'
    161    sex = 'M'
    162    year_of_birth = 1580
    163
    164    assert_person_factory_works(
    165        first_name=first_name,
    166        last_name=last_name,
    167        sex=sex,
    168        year_of_birth=year_of_birth,
    169    )
    170
    171    assert_say_hello_works(
    172        first_name=first_name,
    173        last_name=last_name,
    174        year_of_birth=year_of_birth,
    175    )
    176
    177    john = Person(
    178        first_name=first_name,
    179        last_name=last_name,
    180        sex=sex,
    181        year_of_birth=year_of_birth,
    182    )
    183
    184    reality = john.say_hello()
    185    my_expectation = (
    186        f'Hello, my name is {first_name}'
    187        f' {last_name} and I am'
    188        f' {2026-year_of_birth}.'
    189    )
    190    assert reality == None
    191
    192
    193def 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 in test_john

    184    reality = john.say_hello()
    185    my_expectation = (
    186        f'Hello, my name is {first_name}'
    187        f' {last_name} and I am'
    188        f' {2026-year_of_birth}.'
    189    )
    190    # assert reality == None
    191    assert reality == my_expectation
    192
    193
    194def test_mary():
    

    the test passes.

  • I add an assertion for the say_hello method to test_mary

    194def test_mary():
    195    first_name = 'mary'
    196    last_name = 'public'
    197    sex = 'F'
    198    year_of_birth = 2000
    199
    200    assert_person_factory_works(
    201        first_name=first_name,
    202        last_name=last_name,
    203        sex=sex,
    204        year_of_birth=year_of_birth,
    205    )
    206
    207    assert_say_hello_works(
    208        first_name=first_name,
    209        last_name=last_name,
    210        year_of_birth=year_of_birth,
    211    )
    212
    213    mary = Person(
    214        first_name=first_name,
    215        last_name=last_name,
    216        sex=sex,
    217        year_of_birth=year_of_birth,
    218    )
    219
    220    reality = mary.say_hello()
    221    my_expectation = (
    222        f'Hello, my name is {first_name}'
    223        f' {last_name} and I am'
    224        f' {2026-year_of_birth}.'
    225    )
    226    assert reality == None
    227
    228
    229# 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 in test_mary

    220    reality = mary.say_hello()
    221    my_expectation = (
    222        f'Hello, my name is {first_name}'
    223        f' {last_name} and I am'
    224        f' {2026-year_of_birth}.'
    225    )
    226    # assert reality == None
    227    assert reality == my_expectation
    228
    229
    230# Exceptions seen
    

    the test passes.

  • I add a git commit message in the other terminal

    git commit -am 'add say_hello method'
    

Since the say_hello method is the same as the say_hello function I could have used the say_hello method to call the say_hello function to get the same result

def say_hello(self):
    return say_hello(
        first_name=self.first_name,
        last_name=self.last_name,
        year_of_birth=self.year_of_birth,
    )

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 an instance of the Person class of the person module in the src folder instead of the Person class in tests/test_person.py

    213    # mary = Person(
    214    mary = src.person.Person(
    215        first_name=first_name,
    216        last_name=last_name,
    217        sex=sex,
    218        year_of_birth=year_of_birth,
    219    )
    220
    221    reality = mary.say_hello()
    222    my_expectation = (
    223        f'Hello, my name is {first_name}'
    224        f' {last_name} and I am'
    225        f' {2026-year_of_birth}.'
    226    )
    227    # assert reality == None
    228    assert reality == my_expectation
    229
    230
    231# 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 src/person/__init__.py.


GREEN: make it pass


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

  • I add the name to src/person/__init__.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 Person 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
    

    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
    

    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
    

    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
    

    the terminal is my friend, and shows AttributeError

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

    because the function I just made returns None

    ├── first_name = 'mary'
    ├── last_name = 'public'
    ├── sex = 'F'
    ├── year_of_birth = 2000
    └── mary = src.person.Person(
            first_name=first_name,
            last_name=last_name,
            sex=sex,
            year_of_birth=year_of_birth,
        )
        └── src/
            └── person/
                └── __init__.py
                    └── def Person(
                            first_name, last_name,
                            sex, year_of_birth,
                        ):
                        └── 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 an object

     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
    

    the terminal is my friend, and shows SyntaxError

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

    231# Exceptions seen
    232# AssertionError
    233# NameError
    234# TypeError
    235# AttributeError
    236# SyntaxError
    
  • I change the return statement in the Person object to the pass keyword, in src/person/__init__.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 person functions in src/person/__init__.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
    

    the terminal is my friend, and shows AttributeError

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

    better, I can add an attribute to an object.

  • 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        ):
    21        # return None
    22        pass
    

    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        ):
    22        # return None
    23        pass
    

    the terminal is my friend, and shows TypeError

    TypeError: 'NoneType' object is not callable
    

    because I cannot call None like a function.

  • I change say_hello 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
    

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

    194def test_mary():
    195    first_name = 'mary'
    196    last_name = 'public'
    197    sex = 'F'
    198    year_of_birth = 2000
    199
    200    assert_person_factory_works(
    201        first_name=first_name,
    202        last_name=last_name,
    203        sex=sex,
    204        year_of_birth=year_of_birth,
    205    )
    206
    207    assert_say_hello_works(
    208        first_name=first_name,
    209        last_name=last_name,
    210        year_of_birth=year_of_birth,
    211    )
    212
    213    mary = src.person.Person(
    214        first_name=first_name,
    215        last_name=last_name,
    216        sex=sex,
    217        year_of_birth=year_of_birth,
    218    )
    219
    220    reality = mary.say_hello()
    221    my_expectation = (
    222        f'Hello, my name is {first_name}'
    223        f' {last_name} and I am'
    224        f' {2026-year_of_birth}.'
    225    )
    226    assert reality == my_expectation
    227
    228
    229# Exceptions seen
    
  • I change john in test_john to be an instance of the Person class of the person module in the src folder

    171    assert_say_hello_works(
    172        first_name=first_name,
    173        last_name=last_name,
    174        year_of_birth=year_of_birth,
    175    )
    176
    177    # john = Person(
    178    john = src.person.Person(
    179        first_name=first_name,
    180        last_name=last_name,
    181        sex=sex,
    182        year_of_birth=year_of_birth,
    183    )
    184
    185    reality = john.say_hello()
    186    my_expectation = (
    187        f'Hello, my name is {first_name}'
    188        f' {last_name} and I am'
    189        f' {2026-year_of_birth}.'
    190    )
    191    # assert reality == None
    192    assert reality == my_expectation
    193
    194
    195def 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 src/person/__init__.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 object 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 object 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 object attribute named last_name.

  • I add self.last_name to the __init__ 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 object 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() == Person().say_hello(Person())
    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 tests/test_person.py

    158def test_john():
    159    first_name = 'john'
    160    last_name = 'smith'
    161    sex = 'M'
    162    year_of_birth = 1580
    163
    164    assert_person_factory_works(
    165        first_name=first_name,
    166        last_name=last_name,
    167        sex=sex,
    168        year_of_birth=year_of_birth,
    169    )
    170
    171    assert_say_hello_works(
    172        first_name=first_name,
    173        last_name=last_name,
    174        year_of_birth=year_of_birth,
    175    )
    176
    177    john = 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
    184    reality = john.say_hello()
    185    my_expectation = (
    186        f'Hello, my name is {first_name}'
    187        f' {last_name} and I am'
    188        f' {2026-year_of_birth}.'
    189    )
    190    assert reality == my_expectation
    191
    192
    193def test_mary():
    
  • I change jane in test_jane to be an instance of the Person class of the person module in the src folder

    130    assert_say_hello_works(
    131        first_name=first_name,
    132        last_name=last_name,
    133        year_of_birth=year_of_birth,
    134    )
    135
    136    # jane = Person(
    137    jane = src.person.Person(
    138        first_name=first_name,
    139        last_name=last_name,
    140        sex=sex,
    141        year_of_birth=year_of_birth,
    142    )
    143
    144    # reality = Person.say_hello(
    145    reality = jane.say_hello(
    146        # person=jane,
    147        # first_name=jane.first_name,
    148        # last_name=jane.last_name,
    149        # year_of_birth=jane.year_of_birth,
    150    )
    151    my_expectation = (
    152        f'Hello, my name is {first_name}'
    153        f' {last_name} and I am'
    154        f' {2026-year_of_birth}.'
    155    )
    156    assert reality == my_expectation
    157
    158
    159def test_john():
    

    the test is still green.

  • I remove the commented lines from test_jane

    117def test_jane():
    118    first_name = 'jane'
    119    last_name = 'doe'
    120    sex = 'F'
    121    year_of_birth = 1991
    122
    123    assert_person_factory_works(
    124        first_name=first_name,
    125        last_name=last_name,
    126        sex=sex,
    127        year_of_birth=year_of_birth,
    128    )
    129
    130    assert_say_hello_works(
    131        first_name=first_name,
    132        last_name=last_name,
    133        year_of_birth=year_of_birth,
    134    )
    135
    136    jane = src.person.Person(
    137        first_name=first_name,
    138        last_name=last_name,
    139        sex=sex,
    140        year_of_birth=year_of_birth,
    141    )
    142
    143    reality = jane.say_hello()
    144    my_expectation = (
    145        f'Hello, my name is {first_name}'
    146        f' {last_name} and I am'
    147        f' {2026-year_of_birth}.'
    148    )
    149    assert reality == my_expectation
    150
    151
    152def test_john():
    
  • I change joe in test_joe to be an instance of the Person class of the person module in the src folder

     88    assert_say_hello_works(
     89        first_name=first_name,
     90        last_name=last_name,
     91        year_of_birth=year_of_birth,
     92    )
     93
     94    # joe = Person(
     95    joe = src.person.Person(
     96        first_name=first_name,
     97        last_name=last_name,
     98        sex=sex,
     99        year_of_birth=year_of_birth,
    100    )
    101
    102    # reality = src.person.say_hello(
    103    # reality = Person.say_hello(
    104    reality = joe.say_hello(
    105        # person=joe,
    106        # first_name=joe.first_name,
    107        # last_name=joe.last_name,
    108        # year_of_birth=joe.year_of_birth,
    109    )
    110    my_expectation = (
    111        f'Hello, my name is {first_name}'
    112        f' {last_name} and I am'
    113        f' {2026-year_of_birth}.'
    114    )
    115    assert reality == my_expectation
    116
    117
    118def test_jane():
    

    the test is still green.

  • I remove the commented lines from test_joe

     75def test_joe():
     76    first_name = 'joe'
     77    last_name = 'blow'
     78    sex = 'M'
     79    year_of_birth = 1996
     80
     81    assert_person_factory_works(
     82        first_name=first_name,
     83        last_name=last_name,
     84        sex=sex,
     85        year_of_birth=year_of_birth
     86    )
     87
     88    assert_say_hello_works(
     89        first_name=first_name,
     90        last_name=last_name,
     91        year_of_birth=year_of_birth,
     92    )
     93
     94    joe = src.person.Person(
     95        first_name=first_name,
     96        last_name=last_name,
     97        sex=sex,
     98        year_of_birth=year_of_birth,
     99    )
    100
    101    reality = joe.say_hello()
    102    my_expectation = (
    103        f'Hello, my name is {first_name}'
    104        f' {last_name} and I am'
    105        f' {2026-year_of_birth}.'
    106    )
    107    assert reality == my_expectation
    108
    109
    110def test_jane():
    
  • I remove the Person class from tests/test_person.py

    import src.person
    
    
    def assert_say_hello_works(
            first_name, last_name,
            year_of_birth,
        ):
    

    all the tests are still green because the instances of the Person class that were in tests/test_person.py are now of the Person class in src/person/__init__.py.

    src.person.Person
    src/
    └── person/
        └── __init__.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 src'
    

    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.


extract assert_person_can_say_hello function

test_joe, test_jane, test_john and test_mary use the same process to test the say_hello method, they

instance = src.person.Person(
    first_name=first_name,
    last_name=last_name,
    sex=sex,
    year_of_birth=year_of_birth,
)

reality = instance.say_hello()
my_expectation = (
    f'Hello, my name is {first_name}'
    f' {last_name} and I am'
    f' {2026-year_of_birth}.'
)
assert reality == my_expectation

I can make a function that takes in first_name, last_name and year_of_birth then asserts that the result of the call to the say_hello method with the values is equal to the string with the values of the given parameters.


RED: make it fail


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

  • I add a new function to assert that the result of the call to the say_hello method with the variables is equal to the string with the values of the given parameters, in tests/test_person.py

     4import src.person
     5
     6
     7def assert_person_can_say_hello(
     8        first_name, last_name,
     9        sex, year_of_birth,
    10    ):
    11    instance = src.person.Person(
    12        first_name=first_name,
    13        last_name=last_name,
    14        sex=sex,
    15        year_of_birth=year_of_birth,
    16    )
    17
    18    reality = instance.say_hello()
    19    my_expectation = (
    20        f'Hello, my name is {first_name}'
    21        f' {last_name} and I am'
    22        f' {2026-year_of_birth}.'
    23    )
    24    assert reality != my_expectation
    25
    26
    27def assert_say_hello_works(
    28        first_name, last_name,
    29        year_of_birth,
    30    ):
    
  • I use the assert_person_can_say_hello function in test_joe

    58def test_joe():
    59    first_name = 'joe'
    60    last_name = 'blow'
    61    sex = 'M'
    62    year_of_birth = 1996
    63
    64    assert_person_factory_works(
    65        first_name=first_name,
    66        last_name=last_name,
    67        sex=sex,
    68        year_of_birth=year_of_birth
    69    )
    70
    71    assert_say_hello_works(
    72        first_name=first_name,
    73        last_name=last_name,
    74        year_of_birth=year_of_birth,
    75    )
    76
    77    # joe = src.person.Person(
    78    #     first_name=first_name,
    79    #     last_name=last_name,
    80    #     sex=sex,
    81    #     year_of_birth=year_of_birth,
    82    # )
    83
    84    # reality = joe.say_hello()
    85    # my_expectation = (
    86    #     f'Hello, my name is {first_name}'
    87    #     f' {last_name} and I am'
    88    #     f' {2026-year_of_birth}.'
    89    # )
    90    # assert reality == my_expectation
    91    assert_person_can_say_hello(
    92        first_name=first_name,
    93        last_name=last_name,
    94        sex=sex,
    95        year_of_birth=year_of_birth,
    96    )
    97
    98
    99def test_jane():
    

    the terminal is my friend, and shows AssertionError

    AssertionError: assert 'Hello, my name is joe blow and I am 30.'
                        != 'Hello, my name is joe blow and I am 30.'
    

GREEN: make it pass


I change the assertion in the assert_person_can_say_hello function

 4def assert_person_can_say_hello(
 5        first_name, last_name,
 6        sex, year_of_birth,
 7    ):
 8    instance = src.person.Person(
 9        first_name=first_name,
10        last_name=last_name,
11        sex=sex,
12        year_of_birth=year_of_birth,
13    )
14
15    reality = instance.say_hello()
16    my_expectation = (
17        f'Hello, my name is {first_name}'
18        f' {last_name} and I am'
19        f' {2026-year_of_birth}.'
20    )
21    # assert reality != my_expectation
22    assert reality == my_expectation
23
24
25def assert_say_hello_works(
26        first_name, last_name,
27        year_of_birth,
28    ):

the test passes.

assert_person_can_say_hello(
      first_name=first_name, last_name=last_name,
      sex=sex, year_of_birth=year_of_birth,
) -> None
└── def assert_person_can_say_hello(
        first_name, last_name,
        sex, year_of_birth,
    ):
    ├── instance = src.person.Person(
    │       first_name=first_name,
    │       last_name=last_name,
    │       sex=sex,
    │       year_of_birth=year_of_birth,
    │   )
    │   └── src/person/__init__.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
    ├── reality = instance.say_hello()
    │             └── class Person:
    │                 └── def say_hello(self):
    │                     └── return (
    │                             f'Hello, my name is {self.first_name}'
    │                             f' {self.last_name} and I am'
    │                             f' {2026-self.year_of_birth}.'
    │                         )
    ├── my_expectation = (
    │       f'Hello, my name is {first_name}'
    │       f' {last_name} and I am'
    │       f' {2026-year_of_birth}.'
    │   )
    └── assert reality == my_expectation

REFACTOR: make it better


  • I remove the commented line from assert_person_can_say_hello

     4def assert_person_can_say_hello(
     5        first_name, last_name,
     6        sex, year_of_birth,
     7    ):
     8    instance = src.person.Person(
     9        first_name=first_name,
    10        last_name=last_name,
    11        sex=sex,
    12        year_of_birth=year_of_birth,
    13    )
    14
    15    reality = instance.say_hello()
    16    my_expectation = (
    17        f'Hello, my name is {first_name}'
    18        f' {last_name} and I am'
    19        f' {2026-year_of_birth}.'
    20    )
    21    assert reality == my_expectation
    22
    23
    24def assert_say_hello_works(
    25        first_name, last_name,
    26        year_of_birth,
    27    ):
    
  • I remove the commented lines from test_joe

    58def test_joe():
    59    first_name = 'joe'
    60    last_name = 'blow'
    61    sex = 'M'
    62    year_of_birth = 1996
    63
    64    assert_person_factory_works(
    65        first_name=first_name,
    66        last_name=last_name,
    67        sex=sex,
    68        year_of_birth=year_of_birth
    69    )
    70
    71    assert_say_hello_works(
    72        first_name=first_name,
    73        last_name=last_name,
    74        year_of_birth=year_of_birth,
    75    )
    76
    77    assert_person_can_say_hello(
    78        first_name=first_name,
    79        last_name=last_name,
    80        sex=sex,
    81        year_of_birth=year_of_birth,
    82    )
    83
    84
    85def test_jane():
    
  • I use the assert_person_can_say_hello function in test_jane

     85def test_jane():
     86    first_name = 'jane'
     87    last_name = 'doe'
     88    sex = 'F'
     89    year_of_birth = 1991
     90
     91    assert_person_factory_works(
     92        first_name=first_name,
     93        last_name=last_name,
     94        sex=sex,
     95        year_of_birth=year_of_birth,
     96    )
     97
     98    assert_say_hello_works(
     99        first_name=first_name,
    100        last_name=last_name,
    101        year_of_birth=year_of_birth,
    102    )
    103
    104    # jane = src.person.Person(
    105    #     first_name=first_name,
    106    #     last_name=last_name,
    107    #     sex=sex,
    108    #     year_of_birth=year_of_birth,
    109    # )
    110
    111    # reality = jane.say_hello()
    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    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
    126def test_john():
    

    the test is still green.

  • I remove the commented lines from test_jane

     85def test_jane():
     86    first_name = 'jane'
     87    last_name = 'doe'
     88    sex = 'F'
     89    year_of_birth = 1991
     90
     91    assert_person_factory_works(
     92        first_name=first_name,
     93        last_name=last_name,
     94        sex=sex,
     95        year_of_birth=year_of_birth,
     96    )
     97
     98    assert_say_hello_works(
     99        first_name=first_name,
    100        last_name=last_name,
    101        year_of_birth=year_of_birth,
    102    )
    103
    104    assert_person_can_say_hello(
    105        first_name=first_name,
    106        last_name=last_name,
    107        sex=sex,
    108        year_of_birth=year_of_birth,
    109    )
    110
    111
    112def test_john():
    
  • I use the assert_person_can_say_hello function in test_john

    112def test_john():
    113    first_name = 'john'
    114    last_name = 'smith'
    115    sex = 'M'
    116    year_of_birth = 1580
    117
    118    assert_person_factory_works(
    119        first_name=first_name,
    120        last_name=last_name,
    121        sex=sex,
    122        year_of_birth=year_of_birth,
    123    )
    124
    125    assert_say_hello_works(
    126        first_name=first_name,
    127        last_name=last_name,
    128        year_of_birth=year_of_birth,
    129    )
    130
    131    # john = src.person.Person(
    132    #     first_name=first_name,
    133    #     last_name=last_name,
    134    #     sex=sex,
    135    #     year_of_birth=year_of_birth,
    136    # )
    137
    138    # reality = john.say_hello()
    139    # my_expectation = (
    140    #     f'Hello, my name is {first_name}'
    141    #     f' {last_name} and I am'
    142    #     f' {2026-year_of_birth}.'
    143    # )
    144    # assert reality == my_expectation
    145    assert_person_can_say_hello(
    146        first_name=first_name,
    147        last_name=last_name,
    148        sex=sex,
    149        year_of_birth=year_of_birth,
    150    )
    151
    152
    153def test_mary():
    

    still green.

  • I remove the commented lines from test_john

    112def test_john():
    113    first_name = 'john'
    114    last_name = 'smith'
    115    sex = 'M'
    116    year_of_birth = 1580
    117
    118    assert_person_factory_works(
    119        first_name=first_name,
    120        last_name=last_name,
    121        sex=sex,
    122        year_of_birth=year_of_birth,
    123    )
    124
    125    assert_say_hello_works(
    126        first_name=first_name,
    127        last_name=last_name,
    128        year_of_birth=year_of_birth,
    129    )
    130
    131    assert_person_can_say_hello(
    132        first_name=first_name,
    133        last_name=last_name,
    134        sex=sex,
    135        year_of_birth=year_of_birth,
    136    )
    137
    138
    139def test_mary():
    
  • I use the assert_person_can_say_hello function in test_mary

    139def test_mary():
    140    first_name = 'mary'
    141    last_name = 'public'
    142    sex = 'F'
    143    year_of_birth = 2000
    144
    145    assert_person_factory_works(
    146        first_name=first_name,
    147        last_name=last_name,
    148        sex=sex,
    149        year_of_birth=year_of_birth,
    150    )
    151
    152    assert_say_hello_works(
    153        first_name=first_name,
    154        last_name=last_name,
    155        year_of_birth=year_of_birth,
    156    )
    157
    158    # mary = src.person.Person(
    159    #     first_name=first_name,
    160    #     last_name=last_name,
    161    #     sex=sex,
    162    #     year_of_birth=year_of_birth,
    163    # )
    164
    165    # reality = mary.say_hello()
    166    # my_expectation = (
    167    #     f'Hello, my name is {first_name}'
    168    #     f' {last_name} and I am'
    169    #     f' {2026-year_of_birth}.'
    170    # )
    171    # assert reality == my_expectation
    172    assert_person_can_say_hello(
    173        first_name=first_name,
    174        last_name=last_name,
    175        sex=sex,
    176        year_of_birth=year_of_birth,
    177    )
    178
    179
    180# Exceptions seen
    

    green.

  • I remove the commented lines from test_mary

    139def test_mary():
    140    first_name = 'mary'
    141    last_name = 'public'
    142    sex = 'F'
    143    year_of_birth = 2000
    144
    145    assert_person_factory_works(
    146        first_name=first_name,
    147        last_name=last_name,
    148        sex=sex,
    149        year_of_birth=year_of_birth,
    150    )
    151
    152    assert_say_hello_works(
    153        first_name=first_name,
    154        last_name=last_name,
    155        year_of_birth=year_of_birth,
    156    )
    157
    158    assert_person_can_say_hello(
    159        first_name=first_name,
    160        last_name=last_name,
    161        sex=sex,
    162        year_of_birth=year_of_birth,
    163    )
    164
    165
    166# Exceptions seen
    
  • I add a git commit message in the other terminal

    git commit -am \
    'extract assert_person_can_say_hello function'
    

use assert_equal function

assert_person_can_say_hello, assert_say_hello_works and assert_person_factory_works all assert that something is equal to something else

assert reality == my_expectation

I can use the assert_equal function that takes in two inputs then asserts that they are equal.


RED: make it fail


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

  • I add the assert_equal function to tests/test_person.py

     1import src.person
     2
     3
     4def assert_equal(left, right):
     5    assert left != right
     6
     7
     8def assert_person_can_say_hello(
     9        first_name, last_name,
    10        sex, year_of_birth,
    11    ):
    
  • I use the assert_equal function in assert_person_can_say_hello

     8def assert_person_can_say_hello(
     9        first_name, last_name,
    10        sex, year_of_birth,
    11    ):
    12    instance = src.person.Person(
    13        first_name=first_name,
    14        last_name=last_name,
    15        sex=sex,
    16        year_of_birth=year_of_birth,
    17    )
    18
    19    reality = instance.say_hello()
    20    my_expectation = (
    21        f'Hello, my name is {first_name}'
    22        f' {last_name} and I am'
    23        f' {2026-year_of_birth}.'
    24    )
    25    # assert reality == my_expectation
    26    assert_equal(reality, my_expectation)
    27
    28
    29def assert_say_hello_works(
    30        first_name, last_name,
    31        year_of_birth,
    32    ):
    

    the terminal is my friend, and shows AssertionError

    FAILED ...::test_joe - AssertionError:
        assert 'Hello, my name is joe blow and I am 30.' != ...
    FAILED ...::test_jane - AssertionError:
        assert 'Hello, my name is jane doe and I am 35.' != ...
    FAILED ...::test_john - AssertionError:
        assert 'Hello, my name is john smith and I am 446.' ...
    FAILED ...::test_mary - AssertionError:
        assert 'Hello, my name is mary public and I am 26.' ...
    

GREEN: make it pass


I change the assertion in the assert_equal function

 4def assert_equal(left, right):
 5    # assert left != right
 6    assert left == right
 7
 8
 9def assert_person_can_say_hello(
10        first_name, last_name,
11        sex, year_of_birth,
12    ):

the test passes.


REFACTOR: make it better


  • I remove the commented line from assert_equal

     4def assert_equal(left, right):
     5    assert left == right
     6
     7
     8def assert_say_hello_works(
     9        first_name, last_name,
    10        year_of_birth,
    11    ):
    
  • I use the value of my_expectation in assert_person_can_say_hello without a variable since it is only used once

    19    reality = instance.say_hello()
    20    # my_expectation = (
    21    #     f'Hello, my name is {first_name}'
    22    #     f' {last_name} and I am'
    23    #     f' {2026-year_of_birth}.'
    24    # )
    25    # assert reality == my_expectation
    26    # assert_equal(reality, my_expectation)
    27    assert_equal(
    28        reality,
    29        (
    30            f'Hello, my name is {first_name}'
    31            f' {last_name} and I am'
    32            f' {2026-year_of_birth}.'
    33        )
    34    )
    35
    36
    37def assert_say_hello_works(
    38        first_name, last_name,
    39        year_of_birth,
    40    ):
    

    the test is still green.

  • I do the same thing with reality in assert_person_can_say_hello

    19    # reality = instance.say_hello()
    20    # my_expectation = (
    21    #     f'Hello, my name is {first_name}'
    22    #     f' {last_name} and I am'
    23    #     f' {2026-year_of_birth}.'
    24    # )
    25    # assert reality == my_expectation
    26    # assert_equal(reality, my_expectation)
    27    assert_equal(
    28        # reality,
    29        instance.say_hello(),
    30        (
    31            f'Hello, my name is {first_name}'
    32            f' {last_name} and I am'
    33            f' {2026-year_of_birth}.'
    34        )
    35    )
    36
    37
    38def assert_say_hello_works(
    39        first_name, last_name,
    40        year_of_birth,
    41    ):
    

    still green.

  • I use the instance of the Person class directly without a variable since it is only used once

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

    green and not as easy to read.

  • I use the assert_equal function in assert_say_hello_works

    27def assert_say_hello_works(
    28        first_name, last_name,
    29        year_of_birth,
    30    ):
    31    # reality = src.person.say_hello(
    32    #     first_name=first_name,
    33    #     last_name=last_name,
    34    #     year_of_birth=year_of_birth
    35    # )
    36    # my_expectation = (
    37    #     f'Hello, my name is {first_name}'
    38    #     f' {last_name} and I am'
    39    #     f' {2026-year_of_birth}.'
    40    # )
    41    # assert reality == my_expectation
    42    assert_equal(
    43        src.person.say_hello(
    44            first_name=first_name,
    45            last_name=last_name,
    46            year_of_birth=year_of_birth
    47        ),
    48        (
    49            f'Hello, my name is {first_name}'
    50            f' {last_name} and I am'
    51            f' {2026-year_of_birth}.'
    52        )
    53    )
    54
    55
    56def assert_person_factory_works(
    57        first_name, last_name,
    58        sex, year_of_birth
    59    ):
    

    the tests are still green.

  • I remove the commented lines from assert_say_hello_works

    27def assert_say_hello_works(
    28        first_name, last_name,
    29        year_of_birth,
    30    ):
    31    assert_equal(
    32        src.person.say_hello(
    33            first_name=first_name,
    34            last_name=last_name,
    35            year_of_birth=year_of_birth
    36        ),
    37        (
    38            f'Hello, my name is {first_name}'
    39            f' {last_name} and I am'
    40            f' {2026-year_of_birth}.'
    41        )
    42    )
    43
    44
    45def assert_person_factory_works(
    46        first_name, last_name,
    47        sex, year_of_birth
    48    ):
    
  • I use the assert_equal function in assert_person_factory_works

    45def assert_person_factory_works(
    46        first_name, last_name,
    47        sex, year_of_birth
    48    ):
    49    # reality = src.person.person(
    50    #     first_name=first_name,
    51    #     last_name=last_name,
    52    #     sex=sex,
    53    #     year_of_birth=year_of_birth,
    54    # )
    55    # my_expectation = (
    56    #     f'{first_name}, {last_name},'
    57    #     f' {sex}, {year_of_birth}'
    58    # )
    59    # assert reality == my_expectation
    60    assert_equal(
    61        src.person.person(
    62            first_name=first_name,
    63            last_name=last_name,
    64            sex=sex,
    65            year_of_birth=year_of_birth,
    66        ),
    67        (
    68            f'{first_name}, {last_name},'
    69            f' {sex}, {year_of_birth}'
    70        )
    71    )
    72
    73
    74def test_joe():
    

    still green.

  • I remove the commented lines from assert_person_factory_works

    45def assert_person_factory_works(
    46        first_name, last_name,
    47        sex, year_of_birth
    48    ):
    49    assert_equal(
    50        src.person.person(
    51            first_name=first_name,
    52            last_name=last_name,
    53            sex=sex,
    54            year_of_birth=year_of_birth,
    55        ),
    56        (
    57            f'{first_name}, {last_name},'
    58            f' {sex}, {year_of_birth}'
    59        )
    60    )
    61
    62
    63def test_joe():
    
  • I add a git commit message in the other terminal

    git commit -am \
    'extract assert_equal function'
    

I used the assert_equal function to remove repetition which ended up adding more lines to the code than before I used it.


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.

I want to use it to see the attributes and methods of the Person class.


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

    163    assert_person_can_say_hello(
    164        first_name=first_name,
    165        last_name=last_name,
    166        sex=sex,
    167        year_of_birth=year_of_birth,
    168    )
    169
    170
    171def test_dir_person_class():
    172    assert_equal(
    173        dir(src.person.Person),
    174        None
    175    )
    176
    177
    178# 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 the expectation of the assertion is None.


GREEN: make it pass


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

    171def test_dir_person_class():
    172    assert_equal(
    173        dir(src.person.Person),
    174        # None
    175        [
    176            '__class__', '__delattr__', '__dict__',
    177            '__dir__', '__doc__', '__eq__', ...
    178        ]
    179    )
    180
    181
    182# 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 -+v+v 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 the expectation

    Caution

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

    171def test_dir_person_class():
    172    assert_equal(
    173        dir(src.person.Person),
    174        [
    175            '__class__', '__delattr__', '__dict__',
    176            '__dir__', '__doc__', '__eq__',
    177            '__firstlineno__', '__format__', '__ge__',
    178            '__getattribute__', '__getstate__', '__gt__',
    179            '__hash__', '__init__', '__init_subclass__',
    180            '__le__', '__lt__', '__module__', '__ne__',
    181            '__new__', '__reduce__', '__reduce_ex__',
    182            '__repr__', '__setattr__', '__sizeof__',
    183            '__static_attributes__', '__str__',
    184            '__subclasshook__', '__weakref__', 'say_hello'
    185        ]
    186    )
    187
    188
    189# Exceptions seen
    
  • I add a git commit message in the other terminal

    git commit -am \
    'add test_dir_person_class'
    

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 object

182              '__repr__', '__setattr__', '__sizeof__',
183              '__static_attributes__', '__str__',
184              '__subclasshook__', '__weakref__', 'say_hello'
185          ]
186      )
187
188  def test_dir_person_instance():
189      assert_equal(
190          dir(
191              src.person.Person(
192                  first_name='first_name',
193                  last_name='last_name',
194                  sex='M',
195                  year_of_birth=2026,
196              )
197          ),
198          [
199              '__class__', '__delattr__', '__dict__',
200              '__dir__', '__doc__', '__eq__',
201              '__firstlineno__', '__format__', '__ge__',
202              '__getattribute__', '__getstate__', '__gt__',
203              '__hash__', '__init__', '__init_subclass__',
204              '__le__', '__lt__', '__module__', '__ne__',
205              '__new__', '__reduce__', '__reduce_ex__',
206              '__repr__', '__setattr__', '__sizeof__',
207              '__static_attributes__', '__str__',
208              '__subclasshook__', '__weakref__', 'say_hello'
209          ]
210      )
211
212
213  # 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 the expectation of the assertion of test_dir_person_instance

198          [
199              '__class__', '__delattr__', '__dict__',
200              '__dir__', '__doc__', '__eq__',
201              '__firstlineno__', '__format__', '__ge__',
202              '__getattribute__', '__getstate__', '__gt__',
203              '__hash__', '__init__', '__init_subclass__',
204              '__le__', '__lt__', '__module__', '__ne__',
205              '__new__', '__reduce__', '__reduce_ex__',
206              '__repr__', '__setattr__', '__sizeof__',
207              '__static_attributes__', '__str__',
208              '__subclasshook__', '__weakref__', 'first_name',
209              'last_name', 'say_hello', 'year_of_birth',
210          ]
211      )
212
213
214  # Exceptions seen

the test passes.


REFACTOR: make it better


  • I add sex to the list of attributes and methods of the instance of the Person class

    188def test_dir_person_instance():
    189    assert_equal(
    190        dir(
    191            src.person.Person(
    192                first_name='first_name',
    193                last_name='last_name',
    194                sex='M',
    195                year_of_birth=2026,
    196            )
    197        ),
    198        [
    199            '__class__', '__delattr__', '__dict__',
    200            '__dir__', '__doc__', '__eq__',
    201            '__firstlineno__', '__format__', '__ge__',
    202            '__getattribute__', '__getstate__', '__gt__',
    203            '__hash__', '__init__', '__init_subclass__',
    204            '__le__', '__lt__', '__module__', '__ne__',
    205            '__new__', '__reduce__', '__reduce_ex__',
    206            '__repr__', '__setattr__', '__sizeof__',
    207            '__static_attributes__', '__str__',
    208            '__subclasshook__', '__weakref__', 'first_name',
    209            'last_name', 'say_hello', 'sex', 'year_of_birth',
    210        ]
    211    )
    212
    213
    214# Exceptions seen
    215# AssertionError
    216# NameError
    217# TypeError
    218# AttributeError
    219# 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 src/person/__init__.py

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

    the test passes.

  • I remove the commented lines from the Person class

     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    ):
    
  • I add a git commit message in the other terminal

    git commit -am \
    'add test_dir_person_instance'
    

close the project

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


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.