nonetrace 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nonetrace-0.1.0/LICENSE +21 -0
- nonetrace-0.1.0/PKG-INFO +644 -0
- nonetrace-0.1.0/README.md +617 -0
- nonetrace-0.1.0/pyproject.toml +40 -0
- nonetrace-0.1.0/setup.cfg +4 -0
- nonetrace-0.1.0/src/nonetrace/__init__.py +20 -0
- nonetrace-0.1.0/src/nonetrace/__main__.py +44 -0
- nonetrace-0.1.0/src/nonetrace/analyzer.py +1300 -0
- nonetrace-0.1.0/src/nonetrace/hook.py +102 -0
- nonetrace-0.1.0/src/nonetrace/py.typed +0 -0
- nonetrace-0.1.0/src/nonetrace/tracer.py +182 -0
- nonetrace-0.1.0/src/nonetrace.egg-info/PKG-INFO +644 -0
- nonetrace-0.1.0/src/nonetrace.egg-info/SOURCES.txt +14 -0
- nonetrace-0.1.0/src/nonetrace.egg-info/dependency_links.txt +1 -0
- nonetrace-0.1.0/src/nonetrace.egg-info/top_level.txt +1 -0
- nonetrace-0.1.0/tests/test_nonetrace.py +683 -0
nonetrace-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Khalid Sulaiman Al-Mulaify
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
nonetrace-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nonetrace
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Explains where a None value came from when your Python program crashes because of it.
|
|
5
|
+
Author-email: Khalid Sulaiman Al-Mulaify <khalidpythonist@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: none,debugging,traceback,beginners,errors
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Education
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
21
|
+
Classifier: Topic :: Education
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# nonetrace
|
|
29
|
+
|
|
30
|
+
nonetrace tells you where a None came from. When your program crashes with an error like "'NoneType' object has no attribute 'upper'", Python shows you the line where the None was used. nonetrace adds a few plain-English lines underneath that show where the None was born, why, and what to change.
|
|
31
|
+
|
|
32
|
+
You ran your program, and instead of the result you hoped for, Python printed something like this:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
AttributeError: 'NoneType' object has no attribute 'upper'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If you are new to Python, that message can feel like a door slammed in your face. You never typed the word `NoneType`. You never asked for `None`. So where did it come from?
|
|
39
|
+
|
|
40
|
+
`None` is Python's way of saying "nothing here". It is a real value, like `0` or `""`, but it means "no value at all". The tricky part is that Python hands it to you quietly, without any warning, in these situations:
|
|
41
|
+
|
|
42
|
+
- a function finishes without a `return` line
|
|
43
|
+
- a dictionary lookup with `.get()` finds nothing
|
|
44
|
+
- a search like `re.search()` finds no match
|
|
45
|
+
- you store the result of a method like `.sort()` that changes a list in place
|
|
46
|
+
|
|
47
|
+
The `None` then travels through your program until a later line tries to use it, and only then does Python complain.
|
|
48
|
+
|
|
49
|
+
## Why the traceback is not enough
|
|
50
|
+
|
|
51
|
+
Take this little program. One function builds a greeting, another one shouts it:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
def make_greeting(name):
|
|
55
|
+
greeting = "Hello, " + name + "!"
|
|
56
|
+
print(greeting)
|
|
57
|
+
|
|
58
|
+
def shout(text):
|
|
59
|
+
return text.upper()
|
|
60
|
+
|
|
61
|
+
message = make_greeting("Ada")
|
|
62
|
+
print(shout(message))
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Run it with plain Python and you get:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Hello, Ada!
|
|
69
|
+
Traceback (most recent call last):
|
|
70
|
+
File "C:\code\greet.py", line 9, in <module>
|
|
71
|
+
print(shout(message))
|
|
72
|
+
^^^^^^^^^^^^^^
|
|
73
|
+
File "C:\code\greet.py", line 6, in shout
|
|
74
|
+
return text.upper()
|
|
75
|
+
^^^^^^^^^^
|
|
76
|
+
AttributeError: 'NoneType' object has no attribute 'upper'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Python points at line 6, inside `shout()`. But `shout()` is perfectly fine. The real mistake is up in `make_greeting()`: it prints the greeting instead of returning it, so it hands back `None`, and that `None` is passed along to `shout()`. The traceback shows you where the `None` was used, not where it was born. In a bigger program those two places can be far apart, in different functions or even different files, and that is where beginners lose hours.
|
|
80
|
+
|
|
81
|
+
## What nonetrace does
|
|
82
|
+
|
|
83
|
+
Here is the same program with two extra lines at the top that switch nonetrace on:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import nonetrace
|
|
87
|
+
nonetrace.enable()
|
|
88
|
+
|
|
89
|
+
def make_greeting(name):
|
|
90
|
+
greeting = "Hello, " + name + "!"
|
|
91
|
+
print(greeting)
|
|
92
|
+
|
|
93
|
+
def shout(text):
|
|
94
|
+
return text.upper()
|
|
95
|
+
|
|
96
|
+
message = make_greeting("Ada")
|
|
97
|
+
print(shout(message))
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
And here is what you see now:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
Hello, Ada!
|
|
104
|
+
Traceback (most recent call last):
|
|
105
|
+
File "C:\code\greet_enabled.py", line 12, in <module>
|
|
106
|
+
print(shout(message))
|
|
107
|
+
^^^^^^^^^^^^^^
|
|
108
|
+
File "C:\code\greet_enabled.py", line 9, in shout
|
|
109
|
+
return text.upper()
|
|
110
|
+
^^^^^^^^^^
|
|
111
|
+
AttributeError: 'NoneType' object has no attribute 'upper'
|
|
112
|
+
|
|
113
|
+
nonetrace: 'text' is None on greet_enabled.py line 9, and None has no attribute 'upper'.
|
|
114
|
+
'text' is None because it is a parameter of shout(), and the call on greet_enabled.py line 12 passed in 'message', which is None.
|
|
115
|
+
'message' is None because make_greeting() returned None at greet_enabled.py line 6.
|
|
116
|
+
'message' got that value on greet_enabled.py line 11: message = make_greeting("Ada")
|
|
117
|
+
Why: the function ended without a return statement, so Python gave back None automatically.
|
|
118
|
+
Hint: make_greeting() uses print(). Printing shows a value on the screen, but it does not hand the value back to the code that called make_greeting().
|
|
119
|
+
Suggested fix: replace 'print(greeting)' with 'return greeting' at the end of make_greeting() (greet_enabled.py line 6).
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The normal traceback is still there, exactly as before. Underneath it, nonetrace follows the `None` backwards: `text` was `None` because the call passed in `message`, `message` was `None` because `make_greeting()` returned `None`, and `make_greeting()` returned `None` because it never had a `return` line. Then it tells you what to change.
|
|
123
|
+
|
|
124
|
+
## Install and turn it on
|
|
125
|
+
|
|
126
|
+
Install it with pip:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
pip install nonetrace
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
There are two ways to use it. The first is to add these two lines at the very top of your program:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import nonetrace
|
|
136
|
+
nonetrace.enable()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The second way needs no change to your code at all. Instead of running `python myscript.py`, run:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
python -m nonetrace myscript.py
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Anything you type after the script name is passed to your script as usual, so `python -m nonetrace myscript.py input.txt` works the same way `python myscript.py input.txt` does.
|
|
146
|
+
|
|
147
|
+
## Every picture nonetrace recognizes
|
|
148
|
+
|
|
149
|
+
Each example below is a complete program you can run yourself. The output shown is exactly what nonetrace printed. (Only the folder in the traceback lines was shortened to `C:\code\`.) Every example was run with `python -m nonetrace`.
|
|
150
|
+
|
|
151
|
+
### You forgot the return line
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
def average(numbers):
|
|
155
|
+
result = sum(numbers) / len(numbers)
|
|
156
|
+
|
|
157
|
+
score = average([80, 90, 100])
|
|
158
|
+
print("With the bonus you have", score + 5)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
Traceback (most recent call last):
|
|
163
|
+
File "C:\code\forgot_return.py", line 5, in <module>
|
|
164
|
+
print("With the bonus you have", score + 5)
|
|
165
|
+
~~~~~~^~~
|
|
166
|
+
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
|
|
167
|
+
|
|
168
|
+
nonetrace: 'score' is None on forgot_return.py line 5, and None cannot be used with '+'.
|
|
169
|
+
'score' is None because average() returned None at forgot_return.py line 2.
|
|
170
|
+
'score' got that value on forgot_return.py line 4: score = average([80, 90, 100])
|
|
171
|
+
Why: the function ended without a return statement, so Python gave back None automatically.
|
|
172
|
+
Suggested fix: add 'return result' as the last line of average(), right after forgot_return.py line 2.
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`average()` does the calculation and stores it in `result`, but then simply stops. A function that reaches its end without a `return` gives back `None`. The fix is to add `return result` as its last line.
|
|
176
|
+
|
|
177
|
+
### A return with no value
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
def total_price(prices):
|
|
181
|
+
total = 0
|
|
182
|
+
for price in prices:
|
|
183
|
+
total = total + price
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
cost = total_price([3, 4, 5])
|
|
187
|
+
print("For two people:", cost * 2)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
Traceback (most recent call last):
|
|
192
|
+
File "C:\code\bare_return.py", line 8, in <module>
|
|
193
|
+
print("For two people:", cost * 2)
|
|
194
|
+
~~~~~^~~
|
|
195
|
+
TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'
|
|
196
|
+
|
|
197
|
+
nonetrace: 'cost' is None on bare_return.py line 8, and None cannot be used with '*'.
|
|
198
|
+
'cost' is None because total_price() returned None at bare_return.py line 5.
|
|
199
|
+
'cost' got that value on bare_return.py line 7: cost = total_price([3, 4, 5])
|
|
200
|
+
Why: the function has a return with no value. The bare 'return' on bare_return.py line 5 hands back None.
|
|
201
|
+
Suggested fix: write the value you want after 'return' on bare_return.py line 5, for example 'return total'.
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
A `return` on its own means "stop here and give back nothing", which is `None`. Write the value after it: `return total`.
|
|
205
|
+
|
|
206
|
+
### One branch of an if/else has no return
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
def grade(score):
|
|
210
|
+
if score >= 90:
|
|
211
|
+
return "A"
|
|
212
|
+
elif score >= 80:
|
|
213
|
+
return "B"
|
|
214
|
+
|
|
215
|
+
letter = grade(72)
|
|
216
|
+
print("You got " + letter.upper())
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
Traceback (most recent call last):
|
|
221
|
+
File "C:\code\one_branch.py", line 8, in <module>
|
|
222
|
+
print("You got " + letter.upper())
|
|
223
|
+
^^^^^^^^^^^^
|
|
224
|
+
AttributeError: 'NoneType' object has no attribute 'upper'
|
|
225
|
+
|
|
226
|
+
nonetrace: 'letter' is None on one_branch.py line 8, and None has no attribute 'upper'.
|
|
227
|
+
'letter' is None because grade() returned None at one_branch.py line 4.
|
|
228
|
+
'letter' got that value on one_branch.py line 7: letter = grade(72)
|
|
229
|
+
Why: one code path in the function has no return statement (check your if/else branches).
|
|
230
|
+
This time grade() reached its end on one_branch.py line 4 without meeting a return.
|
|
231
|
+
Suggested fix: make sure every path through grade() ends with 'return <value>', for example by adding an else branch or a final return at the end.
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`grade()` returns something for 90 and above and for 80 and above, but a score of 72 falls through both checks and reaches the end of the function. Add an `else:` branch or a final `return` so that every score gets an answer.
|
|
235
|
+
|
|
236
|
+
### The function returns None on purpose
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
def find_user(users, name):
|
|
240
|
+
if name not in users:
|
|
241
|
+
return None
|
|
242
|
+
return users[name]
|
|
243
|
+
|
|
244
|
+
users = {"ada": "Ada Lovelace", "grace": "Grace Hopper"}
|
|
245
|
+
user = find_user(users, "linus")
|
|
246
|
+
print("Welcome back, " + user.title())
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
Traceback (most recent call last):
|
|
251
|
+
File "C:\code\explicit_none.py", line 8, in <module>
|
|
252
|
+
print("Welcome back, " + user.title())
|
|
253
|
+
^^^^^^^^^^
|
|
254
|
+
AttributeError: 'NoneType' object has no attribute 'title'
|
|
255
|
+
|
|
256
|
+
nonetrace: 'user' is None on explicit_none.py line 8, and None has no attribute 'title'.
|
|
257
|
+
'user' is None because find_user() returned None at explicit_none.py line 3.
|
|
258
|
+
'user' got that value on explicit_none.py line 7: user = find_user(users, "linus")
|
|
259
|
+
Why: the function explicitly returned None, with 'return None' on explicit_none.py line 3.
|
|
260
|
+
That return runs when this condition is true: name not in users
|
|
261
|
+
Suggested fix: check the result before you use it, for example 'if user is not None:', or change find_user() so it returns a real value in that case.
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Sometimes `None` is the function's honest way of saying "I did not find it". That is fine, but the code that calls it has to be ready for that answer. Check with `if user is not None:` before using the result.
|
|
265
|
+
|
|
266
|
+
### dict.get() with a missing key
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
ages = {"Ada": 36, "Grace": 45}
|
|
270
|
+
age = ages.get("Linus")
|
|
271
|
+
print("Next year you will be", age + 1)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
Traceback (most recent call last):
|
|
276
|
+
File "C:\code\dict_get.py", line 3, in <module>
|
|
277
|
+
print("Next year you will be", age + 1)
|
|
278
|
+
~~~~^~~
|
|
279
|
+
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
|
|
280
|
+
|
|
281
|
+
nonetrace: 'age' is None on dict_get.py line 3, and None cannot be used with '+'.
|
|
282
|
+
'age' is None because ages.get("Linus") returns None when the key "Linus" is not in the dictionary.
|
|
283
|
+
'age' got that value on dict_get.py line 2: age = ages.get("Linus")
|
|
284
|
+
dict .get() does not raise an error when a key is missing. It quietly gives back None instead.
|
|
285
|
+
'ages' has no key 'Linus'. The keys it does have are: 'Ada', 'Grace'.
|
|
286
|
+
Suggested fix: check that the key is there first with 'if "Linus" in ages:', or give .get() a default value, like ages.get("Linus", 0).
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`.get()` is polite: when the key is missing it does not crash, it just gives you `None`. Give it a default as the second argument, like `ages.get("Linus", 0)`, or check `if "Linus" in ages:` first.
|
|
290
|
+
|
|
291
|
+
### re.search() found no match
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
import re
|
|
295
|
+
|
|
296
|
+
text = "Order number: ABC"
|
|
297
|
+
match = re.search(r"\d+", text)
|
|
298
|
+
print("Found order", match.group())
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
Traceback (most recent call last):
|
|
303
|
+
File "C:\code\regex_search.py", line 5, in <module>
|
|
304
|
+
print("Found order", match.group())
|
|
305
|
+
^^^^^^^^^^^
|
|
306
|
+
AttributeError: 'NoneType' object has no attribute 'group'
|
|
307
|
+
|
|
308
|
+
nonetrace: 'match' is None on regex_search.py line 5, and None has no attribute 'group'.
|
|
309
|
+
'match' is None because re.search(r"\d+", text) found no match, and re.search() returns None when the pattern does not match.
|
|
310
|
+
'match' got that value on regex_search.py line 4: match = re.search(r"\d+", text)
|
|
311
|
+
re.search() gives back a match object only when it finds the pattern. Otherwise it gives back None.
|
|
312
|
+
Suggested fix: check the result before you use it, for example 'if match:', and make sure the pattern really fits your text.
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`re.search()`, `re.match()` and `re.fullmatch()` give you a match object only when the pattern is found. When nothing matches, they give you `None`. Always check `if match:` before calling `.group()`.
|
|
316
|
+
|
|
317
|
+
### Storing the result of list.sort()
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
scores = [70, 95, 82]
|
|
321
|
+
ranked = scores.sort(reverse=True)
|
|
322
|
+
print("Top score:", ranked[0])
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
Traceback (most recent call last):
|
|
327
|
+
File "C:\code\list_sort.py", line 3, in <module>
|
|
328
|
+
print("Top score:", ranked[0])
|
|
329
|
+
~~~~~~^^^
|
|
330
|
+
TypeError: 'NoneType' object is not subscriptable
|
|
331
|
+
|
|
332
|
+
nonetrace: 'ranked' is None on list_sort.py line 3, and you cannot look inside None with [ ].
|
|
333
|
+
'ranked' is None because scores.sort(reverse=True) sorts 'scores' in place and returns None.
|
|
334
|
+
'ranked' got that value on list_sort.py line 2: ranked = scores.sort(reverse=True)
|
|
335
|
+
Methods like .sort() change the list itself. They do not give back a new list, so the result is None.
|
|
336
|
+
Suggested fix: use 'ranked = sorted(scores, reverse=True)' to get a new sorted list, or call 'scores.sort(reverse=True)' on its own line and keep using 'scores'.
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`.sort()` rearranges the list you already have and returns `None`. If you want a new sorted list you can store, use `sorted(scores, reverse=True)` instead.
|
|
340
|
+
|
|
341
|
+
### Storing the result of append(), extend() or reverse()
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
shopping = ["milk", "bread"]
|
|
345
|
+
shopping = shopping.append("eggs")
|
|
346
|
+
print("Items to buy:", len(shopping))
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
Traceback (most recent call last):
|
|
351
|
+
File "C:\code\list_append.py", line 3, in <module>
|
|
352
|
+
print("Items to buy:", len(shopping))
|
|
353
|
+
^^^^^^^^^^^^^
|
|
354
|
+
TypeError: object of type 'NoneType' has no len()
|
|
355
|
+
|
|
356
|
+
nonetrace: 'shopping' is None on list_append.py line 3, and None has no length.
|
|
357
|
+
'shopping' is None because shopping.append("eggs") changes 'shopping' in place and returns None.
|
|
358
|
+
'shopping' got that value on list_append.py line 2: shopping = shopping.append("eggs")
|
|
359
|
+
Methods like .append() change the list itself. They do not give back a new list, so the result is None.
|
|
360
|
+
Suggested fix: call 'shopping.append("eggs")' on its own line, then keep using 'shopping'.
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
This is one of the most common beginner mistakes of all. `.append()` changes the list and returns `None`, so writing `shopping = shopping.append("eggs")` throws your list away and puts `None` in its place. Just write `shopping.append("eggs")` on its own line. The same is true for `.extend()`, `.insert()`, `.remove()`, `.clear()` and `.reverse()` on lists, and `.update()` on dictionaries. Here is `.reverse()`:
|
|
364
|
+
|
|
365
|
+
```python
|
|
366
|
+
countdown = [1, 2, 3]
|
|
367
|
+
backwards = countdown.reverse()
|
|
368
|
+
for number in backwards:
|
|
369
|
+
print(number)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
```
|
|
373
|
+
Traceback (most recent call last):
|
|
374
|
+
File "C:\code\list_reverse.py", line 3, in <module>
|
|
375
|
+
for number in backwards:
|
|
376
|
+
TypeError: 'NoneType' object is not iterable
|
|
377
|
+
|
|
378
|
+
nonetrace: 'backwards' is None on list_reverse.py line 3, and you cannot loop over None or unpack it.
|
|
379
|
+
'backwards' is None because countdown.reverse() reverses 'countdown' in place and returns None.
|
|
380
|
+
'backwards' got that value on list_reverse.py line 2: backwards = countdown.reverse()
|
|
381
|
+
Methods like .reverse() change the list itself. They do not give back a new list, so the result is None.
|
|
382
|
+
Suggested fix: call 'countdown.reverse()' on its own line, then keep using 'countdown'. If you want a reversed copy instead, use 'countdown[::-1]'.
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### random.shuffle()
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
import random
|
|
389
|
+
|
|
390
|
+
cards = ["Ace", "King", "Queen", "Jack"]
|
|
391
|
+
deck = random.shuffle(cards)
|
|
392
|
+
print("You drew the", deck[0])
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
Traceback (most recent call last):
|
|
397
|
+
File "C:\code\shuffle.py", line 5, in <module>
|
|
398
|
+
print("You drew the", deck[0])
|
|
399
|
+
~~~~^^^
|
|
400
|
+
TypeError: 'NoneType' object is not subscriptable
|
|
401
|
+
|
|
402
|
+
nonetrace: 'deck' is None on shuffle.py line 5, and you cannot look inside None with [ ].
|
|
403
|
+
'deck' is None because random.shuffle(cards) shuffles the list in place and returns None.
|
|
404
|
+
'deck' got that value on shuffle.py line 4: deck = random.shuffle(cards)
|
|
405
|
+
random.shuffle() mixes up the list you give it. It does not give back a new list.
|
|
406
|
+
Suggested fix: call 'random.shuffle(cards)' on its own line, then keep using 'cards', which is now shuffled.
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
`random.shuffle()` mixes up the list you give it and returns `None`. Call it on its own line and keep using `cards`.
|
|
410
|
+
|
|
411
|
+
### Storing the result of print()
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
total = print(19 + 23)
|
|
415
|
+
print("Double it:", total * 2)
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
```
|
|
419
|
+
42
|
|
420
|
+
Traceback (most recent call last):
|
|
421
|
+
File "C:\code\print_assign.py", line 2, in <module>
|
|
422
|
+
print("Double it:", total * 2)
|
|
423
|
+
~~~~~~^~~
|
|
424
|
+
TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'
|
|
425
|
+
|
|
426
|
+
nonetrace: 'total' is None on print_assign.py line 2, and None cannot be used with '*'.
|
|
427
|
+
'total' is None because print() only shows text on the screen, and it always returns None.
|
|
428
|
+
'total' got that value on print_assign.py line 1: total = print(19 + 23)
|
|
429
|
+
print() is for showing things to the person running the program. It does not give anything back to your code.
|
|
430
|
+
Suggested fix: store the value first and print it separately: 'total = 19 + 23', then 'print(total)'.
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
`print()` shows something on the screen, and that is all it does. It gives nothing back to your program. Store the value first, then print it.
|
|
434
|
+
|
|
435
|
+
### A variable that was set to None and never changed
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
names = ["Ada", "Grace", "Linus"]
|
|
439
|
+
winner = None
|
|
440
|
+
for name in names:
|
|
441
|
+
if name.startswith("Z"):
|
|
442
|
+
winner = name
|
|
443
|
+
print("The winner is " + winner.upper())
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
```
|
|
447
|
+
Traceback (most recent call last):
|
|
448
|
+
File "C:\code\direct_none.py", line 6, in <module>
|
|
449
|
+
print("The winner is " + winner.upper())
|
|
450
|
+
^^^^^^^^^^^^
|
|
451
|
+
AttributeError: 'NoneType' object has no attribute 'upper'
|
|
452
|
+
|
|
453
|
+
nonetrace: 'winner' is None on direct_none.py line 6, and None has no attribute 'upper'.
|
|
454
|
+
'winner' is None because it was set to None directly on direct_none.py line 2.
|
|
455
|
+
That line is: winner = None
|
|
456
|
+
The later assignment on direct_none.py line 5 (winner = name) is inside an if block that most likely did not run, so it did not change 'winner'.
|
|
457
|
+
Suggested fix: give 'winner' a real value before you use it, or check 'if winner is not None:' first.
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Starting a variable at `None` and filling it in later is a normal pattern. Here, though, nobody's name starts with "Z", so the line that would have changed `winner` never ran. nonetrace points out both the line that set it to `None` and the assignment that most likely did not happen. Check `if winner is not None:` before using it, or give it a sensible starting value.
|
|
461
|
+
|
|
462
|
+
### A None that nonetrace cannot trace
|
|
463
|
+
|
|
464
|
+
```python
|
|
465
|
+
def log(message):
|
|
466
|
+
print("LOG:", message)
|
|
467
|
+
|
|
468
|
+
def load_user():
|
|
469
|
+
return {"name": "Ada", "nickname": None}
|
|
470
|
+
|
|
471
|
+
log("starting")
|
|
472
|
+
user = load_user()
|
|
473
|
+
nickname = user["nickname"]
|
|
474
|
+
log("user loaded")
|
|
475
|
+
print("Hi, " + nickname.title())
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
```
|
|
479
|
+
LOG: starting
|
|
480
|
+
LOG: user loaded
|
|
481
|
+
Traceback (most recent call last):
|
|
482
|
+
File "C:\code\untraceable.py", line 11, in <module>
|
|
483
|
+
print("Hi, " + nickname.title())
|
|
484
|
+
^^^^^^^^^^^^^^
|
|
485
|
+
AttributeError: 'NoneType' object has no attribute 'title'
|
|
486
|
+
|
|
487
|
+
nonetrace: 'nickname' is None on untraceable.py line 11, and None has no attribute 'title'.
|
|
488
|
+
nonetrace could not find where this None came from.
|
|
489
|
+
'nickname' got its value on untraceable.py line 9: nickname = user["nickname"]
|
|
490
|
+
These are the last functions of yours that returned None. They are only candidates, not a sure answer:
|
|
491
|
+
1. log() returned None at untraceable.py line 2 (called from untraceable.py line 10)
|
|
492
|
+
2. log() returned None at untraceable.py line 2 (called from untraceable.py line 7)
|
|
493
|
+
Suggested fix: add 'print(repr(nickname))' on the lines before untraceable.py line 11 to see where it becomes None, and check 'if nickname is not None:' before you use it.
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Here the `None` was sitting inside a dictionary, and nonetrace cannot see how it got there. Instead of guessing, it says so plainly. It shows the line where the variable got its value, and it lists the last few of your functions that returned `None`, clearly marked as candidates. In this case they are not the culprit: the `None` is simply stored in the dictionary. That honesty matters. A tool that sounds sure when it is not would send you in the wrong direction.
|
|
497
|
+
|
|
498
|
+
## TypeError cases too
|
|
499
|
+
|
|
500
|
+
`AttributeError` is not the only way `None` shows up. The same detective work runs for these errors as well.
|
|
501
|
+
|
|
502
|
+
`'NoneType' object is not subscriptable` happens when you use `[ ]` on `None`:
|
|
503
|
+
|
|
504
|
+
```python
|
|
505
|
+
def load_scores():
|
|
506
|
+
scores = [90, 85, 70]
|
|
507
|
+
|
|
508
|
+
scores = load_scores()
|
|
509
|
+
print("First score:", scores[0])
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
```
|
|
513
|
+
Traceback (most recent call last):
|
|
514
|
+
File "C:\code\te_subscript.py", line 5, in <module>
|
|
515
|
+
print("First score:", scores[0])
|
|
516
|
+
~~~~~~^^^
|
|
517
|
+
TypeError: 'NoneType' object is not subscriptable
|
|
518
|
+
|
|
519
|
+
nonetrace: 'scores' is None on te_subscript.py line 5, and you cannot look inside None with [ ].
|
|
520
|
+
'scores' is None because load_scores() returned None at te_subscript.py line 2.
|
|
521
|
+
'scores' got that value on te_subscript.py line 4: scores = load_scores()
|
|
522
|
+
Why: the function ended without a return statement, so Python gave back None automatically.
|
|
523
|
+
Suggested fix: add 'return scores' as the last line of load_scores(), right after te_subscript.py line 2.
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
`'NoneType' object is not iterable` happens when you loop over `None`:
|
|
527
|
+
|
|
528
|
+
```python
|
|
529
|
+
def get_names():
|
|
530
|
+
names = ["Ada", "Grace"]
|
|
531
|
+
|
|
532
|
+
for name in get_names():
|
|
533
|
+
print(name)
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
```
|
|
537
|
+
Traceback (most recent call last):
|
|
538
|
+
File "C:\code\te_iterable.py", line 4, in <module>
|
|
539
|
+
for name in get_names():
|
|
540
|
+
TypeError: 'NoneType' object is not iterable
|
|
541
|
+
|
|
542
|
+
nonetrace: 'get_names()' is None on te_iterable.py line 4, and you cannot loop over None or unpack it.
|
|
543
|
+
'get_names()' is None because get_names() returned None at te_iterable.py line 2.
|
|
544
|
+
Why: the function ended without a return statement, so Python gave back None automatically.
|
|
545
|
+
Suggested fix: add 'return names' as the last line of get_names(), right after te_iterable.py line 2.
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
`'NoneType' object is not callable` happens when you try to call `None` like a function:
|
|
549
|
+
|
|
550
|
+
```python
|
|
551
|
+
def pick_greeting(language):
|
|
552
|
+
if language == "en":
|
|
553
|
+
return lambda name: "Hello, " + name
|
|
554
|
+
|
|
555
|
+
greet = pick_greeting("fr")
|
|
556
|
+
print(greet("Ada"))
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
```
|
|
560
|
+
Traceback (most recent call last):
|
|
561
|
+
File "C:\code\te_callable.py", line 6, in <module>
|
|
562
|
+
print(greet("Ada"))
|
|
563
|
+
^^^^^^^^^^^^
|
|
564
|
+
TypeError: 'NoneType' object is not callable
|
|
565
|
+
|
|
566
|
+
nonetrace: 'greet' is None on te_callable.py line 6, and you cannot call None like a function.
|
|
567
|
+
'greet' is None because pick_greeting() returned None at te_callable.py line 2.
|
|
568
|
+
'greet' got that value on te_callable.py line 5: greet = pick_greeting("fr")
|
|
569
|
+
Why: one code path in the function has no return statement (check your if/else branches).
|
|
570
|
+
This time pick_greeting() reached its end on te_callable.py line 2 without meeting a return.
|
|
571
|
+
Suggested fix: make sure every path through pick_greeting() ends with 'return <value>', for example by adding an else branch or a final return at the end.
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
`unsupported operand type(s)` happens when you do math with `None`:
|
|
575
|
+
|
|
576
|
+
```python
|
|
577
|
+
def get_price(item):
|
|
578
|
+
prices = {"apple": 3, "pear": 4}
|
|
579
|
+
if item in prices:
|
|
580
|
+
return prices[item]
|
|
581
|
+
|
|
582
|
+
total = 10 + get_price("mango")
|
|
583
|
+
print("Total:", total)
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
```
|
|
587
|
+
Traceback (most recent call last):
|
|
588
|
+
File "C:\code\te_operand.py", line 6, in <module>
|
|
589
|
+
total = 10 + get_price("mango")
|
|
590
|
+
~~~^~~~~~~~~~~~~~~~~~~~
|
|
591
|
+
TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'
|
|
592
|
+
|
|
593
|
+
nonetrace: 'get_price("mango")' is None on te_operand.py line 6, and None cannot be used with '+'.
|
|
594
|
+
'get_price("mango")' is None because get_price() returned None at te_operand.py line 3.
|
|
595
|
+
Why: one code path in the function has no return statement (check your if/else branches).
|
|
596
|
+
This time get_price() reached its end on te_operand.py line 3 without meeting a return.
|
|
597
|
+
Suggested fix: make sure every path through get_price() ends with 'return <value>', for example by adding an else branch or a final return at the end.
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
## What it cannot do
|
|
601
|
+
|
|
602
|
+
It only sees functions written in Python. Built-in functions like `dict.get()`, `re.search()` and `list.sort()` are written in C, and nonetrace cannot watch them from the inside. That is why it recognizes them by their shape instead: when it sees `x = something.sort()`, it knows what `.sort()` does.
|
|
603
|
+
|
|
604
|
+
It slows your program down while it is switched on, because it keeps an eye on every function of yours that returns. That is perfectly fine while you are hunting a bug, but it is meant for debugging, not for programs running in production.
|
|
605
|
+
|
|
606
|
+
Some IDE consoles and notebook tools replace Python's error hook with their own, and then the explanation will not be printed automatically. In that case you can still ask for it yourself inside an `except` block:
|
|
607
|
+
|
|
608
|
+
```python
|
|
609
|
+
import nonetrace
|
|
610
|
+
nonetrace.enable()
|
|
611
|
+
|
|
612
|
+
def make_title(words):
|
|
613
|
+
title = " ".join(words).title()
|
|
614
|
+
|
|
615
|
+
try:
|
|
616
|
+
heading = make_title(["hello", "world"])
|
|
617
|
+
print(heading.center(30))
|
|
618
|
+
except AttributeError as error:
|
|
619
|
+
print(nonetrace.explain(error))
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
```
|
|
623
|
+
nonetrace: 'heading' is None on in_except.py line 9, and None has no attribute 'center'.
|
|
624
|
+
'heading' is None because make_title() returned None at in_except.py line 5.
|
|
625
|
+
'heading' got that value on in_except.py line 8: heading = make_title(["hello", "world"])
|
|
626
|
+
Why: the function ended without a return statement, so Python gave back None automatically.
|
|
627
|
+
Suggested fix: add 'return title' as the last line of make_title(), right after in_except.py line 5.
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
## How it works
|
|
631
|
+
|
|
632
|
+
When you switch nonetrace on, it keeps a small diary. Every time one of your own functions returns `None`, it writes down the function's name, the line where it returned and the line that called it. It keeps only the last 200 entries, and it ignores Python's own library and installed packages so that the diary is about your code. When your program crashes because of `None`, nonetrace reads your source code, finds the variable that was `None` on the crashing line, looks backwards for the line that gave that variable its value, and checks that line against the diary and against a list of well-known built-in functions that return `None`. If the value came from one of your functions, it reads that function to explain why it returned `None`. If it cannot work out the answer, it says so.
|
|
633
|
+
|
|
634
|
+
## API
|
|
635
|
+
|
|
636
|
+
`nonetrace.enable()` switches nonetrace on: it starts a fresh diary and adds the explanation to Python's error messages.
|
|
637
|
+
|
|
638
|
+
`nonetrace.disable()` switches it off again and puts Python's normal error handling back.
|
|
639
|
+
|
|
640
|
+
`nonetrace.explain(error)` returns the explanation for an exception as a string, or `None` if the error has nothing to do with `None`.
|
|
641
|
+
|
|
642
|
+
## License
|
|
643
|
+
|
|
644
|
+
MIT. See the LICENSE file.
|