Skip to main content

nonetrace

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.

You ran your program, and instead of the result you hoped for, Python printed something like this:

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

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?

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:

  • a function finishes without a return line
  • a dictionary lookup with .get() finds nothing
  • a search like re.search() finds no match
  • you store the result of a method like .sort() that changes a list in place

The None then travels through your program until a later line tries to use it, and only then does Python complain.

Why the traceback is not enough

Take this little program. One function builds a greeting, another one shouts it:

def make_greeting(name):
    greeting = "Hello, " + name + "!"
    print(greeting)

def shout(text):
    return text.upper()

message = make_greeting("Ada")
print(shout(message))

Run it with plain Python and you get:

Hello, Ada!
Traceback (most recent call last):
  File "C:\code\greet.py", line 9, in <module>
    print(shout(message))
          ^^^^^^^^^^^^^^
  File "C:\code\greet.py", line 6, in shout
    return text.upper()
           ^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'upper'

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.

What nonetrace does

Here is the same program with two extra lines at the top that switch nonetrace on:

import nonetrace
nonetrace.enable()

def make_greeting(name):
    greeting = "Hello, " + name + "!"
    print(greeting)

def shout(text):
    return text.upper()

message = make_greeting("Ada")
print(shout(message))

And here is what you see now:

Hello, Ada!
Traceback (most recent call last):
  File "C:\code\greet_enabled.py", line 12, in <module>
    print(shout(message))
          ^^^^^^^^^^^^^^
  File "C:\code\greet_enabled.py", line 9, in shout
    return text.upper()
           ^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'upper'

nonetrace: 'text' is None on greet_enabled.py line 9, and None has no attribute 'upper'.
  '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.
  'message' is None because make_greeting() returned None at greet_enabled.py line 6.
  'message' got that value on greet_enabled.py line 11: message = make_greeting("Ada")
  Why: the function ended without a return statement, so Python gave back None automatically.
  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().
  Suggested fix: replace 'print(greeting)' with 'return greeting' at the end of make_greeting() (greet_enabled.py line 6).

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.

Install and turn it on

Install it with pip:

pip install nonetrace

There are two ways to use it. The first is to add these two lines at the very top of your program:

import nonetrace
nonetrace.enable()

The second way needs no change to your code at all. Instead of running python myscript.py, run:

python -m nonetrace myscript.py

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.

Every picture nonetrace recognizes

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.

You forgot the return line

def average(numbers):
    result = sum(numbers) / len(numbers)

score = average([80, 90, 100])
print("With the bonus you have", score + 5)
Traceback (most recent call last):
  File "C:\code\forgot_return.py", line 5, in <module>
    print("With the bonus you have", score + 5)
                                     ~~~~~~^~~
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

nonetrace: 'score' is None on forgot_return.py line 5, and None cannot be used with '+'.
  'score' is None because average() returned None at forgot_return.py line 2.
  'score' got that value on forgot_return.py line 4: score = average([80, 90, 100])
  Why: the function ended without a return statement, so Python gave back None automatically.
  Suggested fix: add 'return result' as the last line of average(), right after forgot_return.py line 2.

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.

A return with no value

def total_price(prices):
    total = 0
    for price in prices:
        total = total + price
    return

cost = total_price([3, 4, 5])
print("For two people:", cost * 2)
Traceback (most recent call last):
  File "C:\code\bare_return.py", line 8, in <module>
    print("For two people:", cost * 2)
                             ~~~~~^~~
TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'

nonetrace: 'cost' is None on bare_return.py line 8, and None cannot be used with '*'.
  'cost' is None because total_price() returned None at bare_return.py line 5.
  'cost' got that value on bare_return.py line 7: cost = total_price([3, 4, 5])
  Why: the function has a return with no value. The bare 'return' on bare_return.py line 5 hands back None.
  Suggested fix: write the value you want after 'return' on bare_return.py line 5, for example 'return total'.

A return on its own means "stop here and give back nothing", which is None. Write the value after it: return total.

One branch of an if/else has no return

def grade(score):
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"

letter = grade(72)
print("You got " + letter.upper())
Traceback (most recent call last):
  File "C:\code\one_branch.py", line 8, in <module>
    print("You got " + letter.upper())
                       ^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'upper'

nonetrace: 'letter' is None on one_branch.py line 8, and None has no attribute 'upper'.
  'letter' is None because grade() returned None at one_branch.py line 4.
  'letter' got that value on one_branch.py line 7: letter = grade(72)
  Why: one code path in the function has no return statement (check your if/else branches).
  This time grade() reached its end on one_branch.py line 4 without meeting a return.
  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.

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.

The function returns None on purpose

def find_user(users, name):
    if name not in users:
        return None
    return users[name]

users = {"ada": "Ada Lovelace", "grace": "Grace Hopper"}
user = find_user(users, "linus")
print("Welcome back, " + user.title())
Traceback (most recent call last):
  File "C:\code\explicit_none.py", line 8, in <module>
    print("Welcome back, " + user.title())
                             ^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'title'

nonetrace: 'user' is None on explicit_none.py line 8, and None has no attribute 'title'.
  'user' is None because find_user() returned None at explicit_none.py line 3.
  'user' got that value on explicit_none.py line 7: user = find_user(users, "linus")
  Why: the function explicitly returned None, with 'return None' on explicit_none.py line 3.
  That return runs when this condition is true: name not in users
  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.

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.

dict.get() with a missing key

ages = {"Ada": 36, "Grace": 45}
age = ages.get("Linus")
print("Next year you will be", age + 1)
Traceback (most recent call last):
  File "C:\code\dict_get.py", line 3, in <module>
    print("Next year you will be", age + 1)
                                   ~~~~^~~
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

nonetrace: 'age' is None on dict_get.py line 3, and None cannot be used with '+'.
  'age' is None because ages.get("Linus") returns None when the key "Linus" is not in the dictionary.
  'age' got that value on dict_get.py line 2: age = ages.get("Linus")
  dict .get() does not raise an error when a key is missing. It quietly gives back None instead.
  'ages' has no key 'Linus'. The keys it does have are: 'Ada', 'Grace'.
  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).

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

re.search() found no match

import re

text = "Order number: ABC"
match = re.search(r"\d+", text)
print("Found order", match.group())
Traceback (most recent call last):
  File "C:\code\regex_search.py", line 5, in <module>
    print("Found order", match.group())
                         ^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'group'

nonetrace: 'match' is None on regex_search.py line 5, and None has no attribute 'group'.
  'match' is None because re.search(r"\d+", text) found no match, and re.search() returns None when the pattern does not match.
  'match' got that value on regex_search.py line 4: match = re.search(r"\d+", text)
  re.search() gives back a match object only when it finds the pattern. Otherwise it gives back None.
  Suggested fix: check the result before you use it, for example 'if match:', and make sure the pattern really fits your text.

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

Storing the result of list.sort()

scores = [70, 95, 82]
ranked = scores.sort(reverse=True)
print("Top score:", ranked[0])
Traceback (most recent call last):
  File "C:\code\list_sort.py", line 3, in <module>
    print("Top score:", ranked[0])
                        ~~~~~~^^^
TypeError: 'NoneType' object is not subscriptable

nonetrace: 'ranked' is None on list_sort.py line 3, and you cannot look inside None with [ ].
  'ranked' is None because scores.sort(reverse=True) sorts 'scores' in place and returns None.
  'ranked' got that value on list_sort.py line 2: ranked = scores.sort(reverse=True)
  Methods like .sort() change the list itself. They do not give back a new list, so the result is None.
  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'.

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

Storing the result of append(), extend() or reverse()

shopping = ["milk", "bread"]
shopping = shopping.append("eggs")
print("Items to buy:", len(shopping))
Traceback (most recent call last):
  File "C:\code\list_append.py", line 3, in <module>
    print("Items to buy:", len(shopping))
                           ^^^^^^^^^^^^^
TypeError: object of type 'NoneType' has no len()

nonetrace: 'shopping' is None on list_append.py line 3, and None has no length.
  'shopping' is None because shopping.append("eggs") changes 'shopping' in place and returns None.
  'shopping' got that value on list_append.py line 2: shopping = shopping.append("eggs")
  Methods like .append() change the list itself. They do not give back a new list, so the result is None.
  Suggested fix: call 'shopping.append("eggs")' on its own line, then keep using 'shopping'.

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

countdown = [1, 2, 3]
backwards = countdown.reverse()
for number in backwards:
    print(number)
Traceback (most recent call last):
  File "C:\code\list_reverse.py", line 3, in <module>
    for number in backwards:
TypeError: 'NoneType' object is not iterable

nonetrace: 'backwards' is None on list_reverse.py line 3, and you cannot loop over None or unpack it.
  'backwards' is None because countdown.reverse() reverses 'countdown' in place and returns None.
  'backwards' got that value on list_reverse.py line 2: backwards = countdown.reverse()
  Methods like .reverse() change the list itself. They do not give back a new list, so the result is None.
  Suggested fix: call 'countdown.reverse()' on its own line, then keep using 'countdown'. If you want a reversed copy instead, use 'countdown[::-1]'.

random.shuffle()

import random

cards = ["Ace", "King", "Queen", "Jack"]
deck = random.shuffle(cards)
print("You drew the", deck[0])
Traceback (most recent call last):
  File "C:\code\shuffle.py", line 5, in <module>
    print("You drew the", deck[0])
                          ~~~~^^^
TypeError: 'NoneType' object is not subscriptable

nonetrace: 'deck' is None on shuffle.py line 5, and you cannot look inside None with [ ].
  'deck' is None because random.shuffle(cards) shuffles the list in place and returns None.
  'deck' got that value on shuffle.py line 4: deck = random.shuffle(cards)
  random.shuffle() mixes up the list you give it. It does not give back a new list.
  Suggested fix: call 'random.shuffle(cards)' on its own line, then keep using 'cards', which is now shuffled.

random.shuffle() mixes up the list you give it and returns None. Call it on its own line and keep using cards.

Storing the result of print()

total = print(19 + 23)
print("Double it:", total * 2)
42
Traceback (most recent call last):
  File "C:\code\print_assign.py", line 2, in <module>
    print("Double it:", total * 2)
                        ~~~~~~^~~
TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'

nonetrace: 'total' is None on print_assign.py line 2, and None cannot be used with '*'.
  'total' is None because print() only shows text on the screen, and it always returns None.
  'total' got that value on print_assign.py line 1: total = print(19 + 23)
  print() is for showing things to the person running the program. It does not give anything back to your code.
  Suggested fix: store the value first and print it separately: 'total = 19 + 23', then 'print(total)'.

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.

A variable that was set to None and never changed

names = ["Ada", "Grace", "Linus"]
winner = None
for name in names:
    if name.startswith("Z"):
        winner = name
print("The winner is " + winner.upper())
Traceback (most recent call last):
  File "C:\code\direct_none.py", line 6, in <module>
    print("The winner is " + winner.upper())
                             ^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'upper'

nonetrace: 'winner' is None on direct_none.py line 6, and None has no attribute 'upper'.
  'winner' is None because it was set to None directly on direct_none.py line 2.
  That line is: winner = None
  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'.
  Suggested fix: give 'winner' a real value before you use it, or check 'if winner is not None:' first.

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.

A None that nonetrace cannot trace

def log(message):
    print("LOG:", message)

def load_user():
    return {"name": "Ada", "nickname": None}

log("starting")
user = load_user()
nickname = user["nickname"]
log("user loaded")
print("Hi, " + nickname.title())
LOG: starting
LOG: user loaded
Traceback (most recent call last):
  File "C:\code\untraceable.py", line 11, in <module>
    print("Hi, " + nickname.title())
                   ^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'title'

nonetrace: 'nickname' is None on untraceable.py line 11, and None has no attribute 'title'.
  nonetrace could not find where this None came from.
  'nickname' got its value on untraceable.py line 9: nickname = user["nickname"]
  These are the last functions of yours that returned None. They are only candidates, not a sure answer:
    1. log() returned None at untraceable.py line 2 (called from untraceable.py line 10)
    2. log() returned None at untraceable.py line 2 (called from untraceable.py line 7)
  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.

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.

TypeError cases too

AttributeError is not the only way None shows up. The same detective work runs for these errors as well.

'NoneType' object is not subscriptable happens when you use [ ] on None:

def load_scores():
    scores = [90, 85, 70]

scores = load_scores()
print("First score:", scores[0])
Traceback (most recent call last):
  File "C:\code\te_subscript.py", line 5, in <module>
    print("First score:", scores[0])
                          ~~~~~~^^^
TypeError: 'NoneType' object is not subscriptable

nonetrace: 'scores' is None on te_subscript.py line 5, and you cannot look inside None with [ ].
  'scores' is None because load_scores() returned None at te_subscript.py line 2.
  'scores' got that value on te_subscript.py line 4: scores = load_scores()
  Why: the function ended without a return statement, so Python gave back None automatically.
  Suggested fix: add 'return scores' as the last line of load_scores(), right after te_subscript.py line 2.

'NoneType' object is not iterable happens when you loop over None:

def get_names():
    names = ["Ada", "Grace"]

for name in get_names():
    print(name)
Traceback (most recent call last):
  File "C:\code\te_iterable.py", line 4, in <module>
    for name in get_names():
TypeError: 'NoneType' object is not iterable

nonetrace: 'get_names()' is None on te_iterable.py line 4, and you cannot loop over None or unpack it.
  'get_names()' is None because get_names() returned None at te_iterable.py line 2.
  Why: the function ended without a return statement, so Python gave back None automatically.
  Suggested fix: add 'return names' as the last line of get_names(), right after te_iterable.py line 2.

'NoneType' object is not callable happens when you try to call None like a function:

def pick_greeting(language):
    if language == "en":
        return lambda name: "Hello, " + name

greet = pick_greeting("fr")
print(greet("Ada"))
Traceback (most recent call last):
  File "C:\code\te_callable.py", line 6, in <module>
    print(greet("Ada"))
          ^^^^^^^^^^^^
TypeError: 'NoneType' object is not callable

nonetrace: 'greet' is None on te_callable.py line 6, and you cannot call None like a function.
  'greet' is None because pick_greeting() returned None at te_callable.py line 2.
  'greet' got that value on te_callable.py line 5: greet = pick_greeting("fr")
  Why: one code path in the function has no return statement (check your if/else branches).
  This time pick_greeting() reached its end on te_callable.py line 2 without meeting a return.
  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.

unsupported operand type(s) happens when you do math with None:

def get_price(item):
    prices = {"apple": 3, "pear": 4}
    if item in prices:
        return prices[item]

total = 10 + get_price("mango")
print("Total:", total)
Traceback (most recent call last):
  File "C:\code\te_operand.py", line 6, in <module>
    total = 10 + get_price("mango")
            ~~~^~~~~~~~~~~~~~~~~~~~
TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'

nonetrace: 'get_price("mango")' is None on te_operand.py line 6, and None cannot be used with '+'.
  'get_price("mango")' is None because get_price() returned None at te_operand.py line 3.
  Why: one code path in the function has no return statement (check your if/else branches).
  This time get_price() reached its end on te_operand.py line 3 without meeting a return.
  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.

What it cannot do

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.

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.

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:

import nonetrace
nonetrace.enable()

def make_title(words):
    title = " ".join(words).title()

try:
    heading = make_title(["hello", "world"])
    print(heading.center(30))
except AttributeError as error:
    print(nonetrace.explain(error))
nonetrace: 'heading' is None on in_except.py line 9, and None has no attribute 'center'.
  'heading' is None because make_title() returned None at in_except.py line 5.
  'heading' got that value on in_except.py line 8: heading = make_title(["hello", "world"])
  Why: the function ended without a return statement, so Python gave back None automatically.
  Suggested fix: add 'return title' as the last line of make_title(), right after in_except.py line 5.

How it works

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.

API

nonetrace.enable() switches nonetrace on: it starts a fresh diary and adds the explanation to Python's error messages.

nonetrace.disable() switches it off again and puts Python's normal error handling back.

nonetrace.explain(error) returns the explanation for an exception as a string, or None if the error has nothing to do with None.

License

MIT. See the LICENSE file.

Release files for nonetrace 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nonetrace 0.1.0
File Size Uploaded
nonetrace-0.1.0.tar.gz 38.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nonetrace 0.1.0
File Interpreter ABI Platform
nonetrace-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 63.9 kB

Release files / nonetrace-0.1.0.tar.gz

Download URL nonetrace-0.1.0.tar.gz
Size 38.0 kB
Tags Source
SHA-256 checksum
How to use checksums
3bf105d80f0c2500086526ab758ceaaa58c96bc656906eb04a34d4c12a4a6b09
BLAKE2b-256 checksum
How to use checksums
161928c3999c5473346f86451f95ea856ae8e6a566eb4b31f79a22270ff14817
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / nonetrace-0.1.0-py3-none-any.whl

Download URL nonetrace-0.1.0-py3-none-any.whl
Size 26.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0acd158021707572c42d4085bd4894883c0b4c112bf7bd2f39070cd767f5d012
BLAKE2b-256 checksum
How to use checksums
31966b3859030b1263e1bb931bde2d4e14a8d0fad6cfae423ea62b2e8da4a828
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page