how to make a person with an object
The factory and say_hello functions use three of the same inputs
first_namelast_nameyear_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
1import src.person
2
3
4def assert_equal(left, right):
5 assert left == right
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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
the terminal shows I am in the
personfolder.../pumping_python/personI open
test_person.pyfrom thetestsfolderI use pytest-watcher to run the tests automatically
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
I add an object definition for
Person1import src.person 2 3 4class Person: 5 6 pass 7 8 9def assert_say_hello_works( 10 first_name, last_name, 11 year_of_birth, 12 ):
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 Nonewhich 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_namein parentheses so that the __init__ method can take inputthe 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
selfas the first argumentselfis Python convention, I can use any name I want.selfis 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'?selfis 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 Nonewhich 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_nameto the definition of __init__the terminal is my friend, and shows TypeError
TypeError: Person.__init__() got an unexpected keyword argument 'sex'selfis 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 Nonewhich 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
sexto the definition of the __init__ methodthe terminal is my friend, and shows TypeError
TypeError: Person.__init__() got an unexpected keyword argument 'year_of_birth'selfis 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 Nonewhich 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_birthto the definition of the __init__ methodthe 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
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
joein test_joethe terminal is my friend, and shows AttributeError
AttributeError: 'Person' object has no attribute 'first_name'because there is nothing named
first_namein the Person object, so Python cannot reach thefirst_nameattribute ofjoe.
GREEN: make it pass
I add
self.first_nameto the __init__ method of the Person objectthe terminal still shows AttributeError because all I have done is add a reference to the
first_name, not defined it.I point
self.first_nameto the value forfirst_namewhen the __init__ method is calledthe 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_namein the Person object, so Python cannot reach thelast_nameattribute ofjoe.I add
self.last_nameand point it to the value forlast_namewhen the __init__ method is calledthe terminal is my friend, and shows AttributeError
AttributeError: 'Person' object has no attribute 'year_of_birth'because there is nothing named
year_of_birthin the Person object, so Python cannot reach theyear_of_birthattribute ofjoe.I add
self.year_of_birthand point it to the value foryear_of_birthwhen the __init__ constructor method is calledthe 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 Noneselfis the instance of the class,joein 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_helloin test_joe to a call to the say_hello method of the Person objectthe 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_helloto the Person objectthe 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_nameto the method definitionthe 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_nameto the method definitionthe 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_birthto the method definitionthe 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 NonePerson.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
personto the method definition for say_hellothe 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
personkeyword argument to the call to the say_hello method from test_janethe 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 NonePerson.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_nameandyear_of_birtharguments from the call to the say_hello method from test_joe since they are repetitions of the object attributesthe 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_nameandyear_of_birthfrom the definition of the say_hello methodthe 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_nameandyear_of_birtharguments from the call from test_janethe 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
selfto 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 methodthe test passes.
I change the call to
Person.say_hellofrom 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 attributesthe terminal is my friend, and shows TypeError
TypeError: Person.say_hello() missing 1 required positional argument: 'person'I make
personoptional in the say_hello methodthe terminal is my friend, and shows AttributeError
AttributeError: 'NoneType' object has no attribute 'first_name'I change
person.toself.in the return statementthe terminal is my friend, and shows NameError
NameError: name 'self' is not definedI add
selfto the parenthesesthe 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 Noneselfis the instance of the class,instancein this caseinstance.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()isinstance = 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
joeas input to the say_hello method since it isself.I remove
person=janefrom the call to the say_hello method from test_jane because the say_hello method is in the Person objectthe 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.' == NoneI change my expectation to match
realityin test_johnthe 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.' == NoneI change my expectation to match
realityin test_marythe test passes.
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
I go back to the terminal where the tests are running
I change
maryin test_mary to be an instance of the Person object of thepersonmodule in thesrcfolder instead of the Person object intests/test_person.pythe 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 add the name to
src/person/__init__.py1Person 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 definedI point
Personto None to define it1# 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 callablebecause I cannot call None like a function.
I change
Personto a function1# 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
Personfunction got called with a name (first_name) that is not in the parentheses of its definition.I add
first_nameto the parentheses1# Person 2# Person = None 3# def Person(): 4def Person(first_name): 5 return Nonethe terminal is my friend, and shows TypeError
TypeError: Person() got an unexpected keyword argument 'last_name'. Did you mean 'first_name'?because the
Personfunction got called with a name (last_name) that is not in the parentheses of its definition.I add
last_nameto the parentheses1# Person 2# Person = None 3# def Person(): 4# def Person(first_name): 5def Person(first_name, last_name): 6 return Nonethe terminal is my friend, and shows TypeError
TypeError: Person() got an unexpected keyword argument 'sex'because the
Personfunction got called with a name (sex) that is not in the parentheses of its definition.I add
sexto the parentheses1# 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 Nonethe terminal is my friend, and shows TypeError
TypeError: Person() got an unexpected keyword argument 'year_of_birth'because the
Personfunction got called with a name (year_of_birth) that is not in the parentheses of its definition.I add
year_of_birthto the parentheses1# 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 Nonethe 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 Nonereality = mary.say_hello() reality = None.say_hello()which raises AttributeError since None does not have anything named
say_helloin it.I change
Personto an object1# 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 Nonethe terminal is my friend, and shows SyntaxError
SyntaxError: 'return' outside functionI add SyntaxError to the list of Exceptions seen, in
tests/test_person.pyI change the return statement in the
Personobject to the pass keyword, insrc/person/__init__.py1# 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 definedbecause the only definitions for
first_nameare in the say_hello and person functions insrc/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
selfto the constructor methodthe terminal is my friend, and shows AttributeError
AttributeError: 'Person' object has no attribute 'say_hello'I add the name to the Person object
the terminal is my friend, and shows NameError
NameError: name 'say_hello' is not definedI point
say_helloto None to define itthe terminal is my friend, and shows TypeError
TypeError: 'NoneType' object is not callablebecause I cannot call None like a function.
I change
say_helloto a methodTypeError: Person.say_hello() takes 0 positional arguments but 1 was givenI 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 remove the commented lines from test_mary in
tests/test_person.pyI change
johnin test_john to be an instance of the Person object of thepersonmodule in thesrcfolderthe terminal is my friend, and shows AssertionError
AssertionError: assert 'Hello, my name ... and I am ZZ.' == 'Hello, my name ...and I am AAA.'I change the return statement of the say_hello method to return the input, in
src/person/__init__.pythe terminal is my friend, and shows AssertionError
AssertionError: assert <src.person.Person object at 0xffffb012cd34> == 'Hello, my name is mary public and I am ZZ.'because
argumentis an instance (a copy) of the Person object.I change the return statement to use object attributes in an f-string
the terminal is my friend, and shows AttributeError
AttributeError: 'Person' object has no attribute 'first_name'because I have not defined an object attribute named
first_name.I add
self.first_nameto the __init__ constructor methodthe 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 an object attribute named
last_name.I add
self.last_nameto the __init__ methodthe terminal is my friend, and shows AttributeError
AttributeError: 'Person' object has no attribute 'year_of_birth'because I have not defined an object attribute named
year_of_birth.I add
self.year_of_birthto the __init__ constructor methodthe test passes.
I change
argumenttoselfin the say_hello method to follow Python conventionthe 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 meansinstance = 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.pyI change
janein test_jane to be an instance of the Person object of thepersonmodule in thesrcfolderthe test is still green.
I remove the commented lines from test_jane
I change
joein test_joe to be an instance of the Person object of thepersonmodule in thesrcfolderthe test is still green.
I remove the commented lines from test_joe
I remove the Person object from
tests/test_person.pyimport 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 object that were in
tests/test_person.pyare now of the Person object insrc/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_birthI add a git commit message in the other terminal
the terminal shows a summary of the changes then goes back to the command line.
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
make an instance of the Person object with values for
first_name,last_name,sexandyear_of_birthcall the say_hello method of the Person object with the values of
first_name,last_nameandyear_of_birthmake a string with the values of
first_name,last_nameandyear_of_birthassert that the result of the call to the say_hello method is equal to the string
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.pyI use the assert_person_can_say_hello function in test_joe
the terminal is my friend, and shows AssertionError
AssertionError: assert 'Hello, my name is joe blow and I am XX.' != 'Hello, my name is joe blow and I am XX.'
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
I remove the commented lines from test_joe
I use the assert_person_can_say_hello function in test_jane
the test is still green.
I remove the commented lines from test_jane
I use the assert_person_can_say_hello function in test_john
still green.
I remove the commented lines from test_john
I use the assert_person_can_say_hello function in test_mary
green.
I remove the commented lines from test_mary
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.py1import 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 remove the commented line from assert_equal
I use the value of
my_expectationin assert_person_can_say_hello without a variable since it is only used oncethe test is still green.
I do the same thing with
realityin assert_person_can_say_hellostill green.
I use the instance of the Person object directly without a variable since it is only used once
green and not as easy to read.
I use the assert_equal function in assert_say_hello_works
the tests are still green.
I remove the commented lines from assert_say_hello_works
I use the assert_equal function in assert_person_factory_works
still green.
I remove the commented lines from assert_person_factory_works
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.pythe terminal is my friend, and shows AssertionError
AssertionError: assert ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', ...] == Nonebecause 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 diffI 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
realityandmy_expectationand it shows AssertionErrorE ...Full output truncated (31 lines hidden), use '-vv' to showI press w on the keyboard in the terminal where the tests are running, to show the menu for pytest-watcher and it shows
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: -vvI 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.
the test passes.
The __init__ and say_hello methods I defined are in the list of attributes and methods.
There are names in the list that I did not define, which leads to the question of where did they come from?
The attributes I defined in the __init__ method are not in the list, because the test called dir on
src.person.Personwhich is the object, not an instance of the 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
sexto the list of attributes and methods of the instance of the Person objectthe 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
sexattribute is not defined anywhere in the Person object.I add
self.sexto the __init__ method of the Person object insrc/person/__init__.pythe 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 ):
close the project
I close
src/person/__init__.pyandtests/test_person.pyI 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
personthe terminal shows
...\pumping_pythonI am back in the
pumping_pythondirectory.
review
I ran tests to write an object that makes a person when given
first_name,last_name,sexandyear_of_birthand has a method so I do not have to pass the same values every time I want to do something with a person.My tests have problems
each test is now the same three tests. There has to be a way that I can use one test for all the people.
the tests with the dir built-in function both require the expectation to be in alphabetical order and can break when the Python version changes.
code from the 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.