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

    terminal
    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

    terminal
    uv run pytest-watcher . --now --patterns "**/*.py"
    

    the terminal shows

    tests/test_person.py ....                           [100%]
    
    =================== 4 passed in A.BCs ====================
    

add Person object

I made a function that makes a string for 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 for 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 object so it can take arguments

    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

    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

    • 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__

    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

    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

    the test passes.


REFACTOR: make it better


  • I remove the commented lines from the Person object

  • I open a new terminal then change directories to person

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

    terminal
    git commit -am 'add Person object'
    

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



GREEN: make it pass


  • I add self.first_name to the __init__ method of the Person object

    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

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

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

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

REFACTOR: make it better


  • I remove the commented line from the Person object

  • I change the call to src.person.say_hello in test_joe to a call to the say_hello method of the Person object

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

  • I add a method definition for say_hello to the Person object

    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

    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

    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

    the terminal is my friend, and shows AssertionError

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

    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 XX.'
    
  • I add a call to the say_hello method of the Person object in test_jane

    the terminal is my friend, and shows AssertionError

    AssertionError: assert 'Hello, my name ... and I am XX.'
                        == 'Hello, my name ... and I am YY.'
    
  • I change the return statement to an f-string like I did with the say_hello function

    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 YY.'
    
  • I add an instance (copy) of the Person object to the call to the say_hello method from test_joe because the instance has the attributes I use in the method

    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

    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

    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

    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

    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

    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

    the test passes. This is still a repetition, I give an instance (copy) of the Person object 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 object so its copies also have the say_hello method

    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

    the test passes.

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

    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

    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

    the terminal is my friend, and shows NameError

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

    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

    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 object

    the test is still green.

  • I add an assertion for the say_hello method to test_john

    the terminal is my friend, and shows AssertionError

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

    the test passes.

  • I add an assertion for the say_hello method to test_mary

    the terminal is my friend, and shows AssertionError

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

    the test passes.

  • I add a git commit message in the other terminal

    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 object

RED: make it fail



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

  • 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

    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 object

    the terminal is my friend, and shows NameError

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

    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

    the terminal shows TypeError

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

    the terminal is my friend, and shows AssertionError

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

    the test passes.


REFACTOR: make it better


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



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



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

    the terminal is my friend, and shows AssertionError

    FAILED ...::test_joe - AssertionError:
        assert 'Hello, my name is joe blow and I am XX.' != ...
    FAILED ...::test_jane - AssertionError:
        assert 'Hello, my name is jane doe and I am YY.' != ...
    FAILED ...::test_john - AssertionError:
        assert 'Hello, my name is john smith and I am AAA.' ...
    FAILED ...::test_mary - AssertionError:
        assert 'Hello, my name is mary public and I am ZZ.' ...
    

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

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


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

    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

    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

    terminal
    [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

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

  • I add a git commit message in the other terminal

    terminal
    git commit -am \
    'add test_dir_person_object'
    

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 object

    the terminal is my friend, and shows AssertionError

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

  • I add self.sex to the __init__ method of the Person object in src/person/__init__.py

    the test passes.

  • I remove the commented lines from the Person object

     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

    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

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