Copyright 2017-2024 Jason Ross, All Rights Reserved

Python - the error reporting isn't always great

This is an error that turns up occasionally when you're developing code in Python. It started happening in some code I was working on recently, and all of the articles I found when I was looking for the cause said the same thing; that you're trying to assign something to a value that's None. Probably something with an index or key, like a list or dictionary.

This didn't seem to match what I saw in my system at the time, but after looking much more closely it was right, although not for obvious reasons.

I wrote some code to duplicate the error, below:

import traceback
from typing import Dict


def make_details() -> Dict[str, str]:

    common = {"legs": 4, "tails": 1}

    result = {"first": "cat"}
    result = result.update(common)
    result["something_else"] = "More details"

    return result


if __name__ == "__main__":

    print("Starting")

    try:
        details = make_details()
    except BaseException as e:
        print(traceback.format_exc())

    print("Done")

Running this gave the following output:

/home/jason/.cache/pypoetry/virtualenvs/assigndemo-DpmIJ2mw-py3.11/bin/python /media/jason/PycharmProjects/assigndemo/main.py 
Starting
Traceback (most recent call last):
  File "/media/jason/PycharmProjects/assigndemo/main.py", line 21, in <module>
    details = make_details()
              ^^^^^^^^^^^^^^
  File "/media/jason/PycharmProjects/assigndemo/main.py", line 11, in make_details
    result["something_else"] = "More details"
    ~~~~~~^^^^^^^^^^^^^^^^^^
TypeError: 'NoneType' object does not support item assignment

Done

Process finished with exit code 0

This is a much more modern Python interpreter than the one I was using when I first saw the error - the original was reporting the error on line 32:

        details = make_details()

That was even more confusing, because details was declared and defined here, and make_details() should have been returning a value.

So, it looked like the error reporting in the Python interpreter was, shall we say, vague. The exception was detected at line 32 (above), but it was actually generated somewhere further down the call stack, presumably inside the make_details() function.

Thankfully, the error reporting in Python has improved over time, and line 11 is where the error is detected, even though really it's a symptom, not the actual problem.

    result["something_else"] = "More details"

The real problem is line 10:

    result = result.update(common)

It might not be immediately obvious, but dict.update() in an in-place function, which changes result during the call. Because of this it returns None, which is the actual cause of the problem.

Changing this line to:

    result.update(common)

fixes the problem.

Summary

This problem was one where the answer on the internet was correct, but the cause was harder to find in the code. A simple mistake, where the result of a function that returns None was assigned to a variable, deleted a valid dictionary. This made the following assignment to an indexed value invalid, which is what the error described.

The original code reported the error as occurring in the wrong place, where it was first encountered instead of where it was thrown. If there's nothing else to learn from this, it's that older versions of Python's error detection weren't always great, and so you should always use the newest version you can. Trust me, it'll save you time by at least getting you closer to the source of the problems.

Made In YYC

Made In YYC
Made In YYC

Hosted in Canada by CanSpace Solutions