Default warning formatting improvements

Introduction

Recent python versions have made some very nice improvements to the readability of exception messages. I think, that the current default warning formatting isn’t very good and could use some polish.

Compare the current warning formatting:

/usr/lib/python3.11/site-packages/the_package/the_module/the_file.py:6: SuperImportantWarning: The warning message is the most important part.
  warnings.warn(

And exception formatting:

Traceback (most recent call last):
  File "/usr/lib/python3.11/site-packages/the_package/the_module/__main__.py", line 6, in <module>
    print(oops.x + something_else)
          ^^^^^^
AttributeError: 'NoneType' object has no attribute 'x'

P.S. This isn't a final proposal. I just wanted to get the conversation started. Any criticisms/comments/suggestions are welcome!

Proposal

I see 2 issues with the current warning formatting

  1. The current format of

    {file_path}:{line_no}: {warning_type}: {warning_message}
    
    • puts the actually important information (warning_type and warning_message) at the end of the line, after the file_path and line_no.
    • doesn’t follow the existing exception stack trace formatting (new users might not understand that the 8 in foo.py:8: refers to the line number)
  2. In the most common case, when the warning was raised by a plain warnings.warn("message") without specifying a stacklevel, the source line is either redundant (contains the same message string, that is already part of the warning) or completely useless (just the function call to warnings.warn).

To fix these issues, I propose to

  1. Change the default formatting to more closely resemble the current exception format. Something along the lines of
    {warning_type}: {warning_message}
      File "{file_path}", line {line_no}, in {source_location}
    
  2. Change the default value of stacklevel from 1 to None and don’t print the source line, when the stacklevel isn’t explicitly set.

With the proposed changes, warning messages will look something like this:

Before
/usr/lib/python3.11/site-packages/package/file.py:5: UserWarning: Source line information is redundant for most warnings.
  warnings.warn("Source line information is redundant for most warnings.")
After
UserWarning: Source line information is redundant for most warnings.
  File "/usr/lib/python3.11/site-packages/package/file.py", line 5, in foo

Before
/usr/lib/python3.11/site-packages/package/file.py:6: SuperImportantWarning: The warning message is the most important part.
  warnings.warn(
After
SuperImportantWarning: The warning message is the most important part.
  File "/usr/lib/python3.11/site-packages/package/file.py", line 6, in foo

Before
/usr/lib/python3.11/site-packages/package/file.py:10: DeprecationWarning: The source info is only useful when stacklevel is used.
    bar(deprecated_baz().boo())
After
DeprecationWarning: The source info is only useful when stacklevel is used.
  File "/usr/lib/python3.11/site-packages/package/file.py", line 10, in foo
    bar(deprecated_baz().boo())
        ^^^^^^^^^^^^^^^^

Open questions

Do the proposed changes break backwards compatibility?

  • Is the warning formatting part of the python “API”? I am assuming, that it’s not, since recent python versions have changed the exception formatting.
  • Is the default value of stacklevel in warnings.warn part of the API? I couldn’t come up with any currently valid code, that would be broken by changing the default value of stacklevel to None.

What should be the new format layout for warnings?

  • Should the File ... line appear before or after the WarningType: message line? In the current proposal, the file is printed after the warning message, but this is actually different from how exceptions are formatted. I prefer the
    DeprecationWarning: You shouldn't do that, use wow.blam() instead
      File "/path/to/file.py", line XX, in foo
        bar(deprecated_baz().boo())
            ^^^^^^^^^^^^^^^^
    
    format, but I can see some valid arguments for a slightly more verbose format, that is closer to the current exception format:
    Traceback:
      File "/path/to/file.py", line XX, in foo
        bar(deprecated_baz().boo())
            ^^^^^^^^^^^^^^^^
    DeprecationWarning: You shouldn't do that, use wow.blam() instead
    
    or maybe without the Traceback level, since it’s not a full traceback
    File "/path/to/file.py", line XX, in foo
      bar(deprecated_baz().boo())
          ^^^^^^^^^^^^^^^^
      DeprecationWarning: You shouldn't do that, use wow.blam() instead
    
10 Likes

I think this is a good proposal. I like the style of the final example best.

This could perhaps do without a PEP, just an issue and PR should be enough.

The default format could not be changed without an explicit opt-in, for compatibility.

Note that file:line:info is a standard format produced by many tools (compilers, linters…) and recognized by many other tools (vim quickfix, etc).

2 Likes

The default format could not be changed without an explicit opt-in, for compatibility.

Do you have any concrete examples of any use cases, that would be broken by the proposed changes? I was also originally worried of breaking compatibility, but after some thought, I couldn’t come up with any real world examples where somebody would want to automatically parse stderr for warnings. I think, that it’s pretty commonly understood, that stderr is for human consumption, not machine-readable data.

Also, as I’ve previously mentioned. Recent python versions changed the exception formatting (a bit), so there is already precedent for this kind of compatibility breakage.

I am totally fine with making this change opt-out (e.g. by setting OLDPYTHONWARNINGS=1 or something like that), but making it opt-in kind of defeats the purpose since we have no way to deprecate the old warning formatting and eventually make the new format the default. Making the new format opt-in will leave it stuck in this limbo state forever (assuming we don’t break compatibility at some later point).

The main point of my proposal is to offer a better default, not to offer more formatting options. I am pretty sure, that you could already accomplish what I am proposing today by monkeypatching warnings.formatwarning, but realistically, nobody is going to do that, because it’s way too intrusive.

Note that file:line:info is a standard format…

Yes, I am aware. However, python exceptions currently don’t follow this format and this format has a few UX problems, that I’ve outlined in the original post. My main motivation for this proposal is to bring exception and warning formatting closer together (and make them better).

If you think, that {file}:{line}:{info} is better than File "{file}", line {line}, in {func}..., that is fine too. But then exceptions should also use the same {file}:{line}:{info} format. I personally don’t think that {file}:{line}:{info} is a good format, but my point is that Python shouldn’t have 2 different formats for indicating the source locations for warnings/exceptions.

Yes, humans use tools such as IDEs that automatically link to the offending file and line:

This makes it much easier for the human to find the problem.

Without the hyperlink, I need to manually open the file (by copying and pasting or typing the name), then check the line number, and type that in to jump there.


Perhaps using colour could help? It would need to detect tty to avoid breaking piped output. Compare recent work in pip:

Yes, humans use tools such as IDEs that automatically link to the offending file and line:

Sorry, if I wasn’t clear. By “breaking compatibility” I meant programs that worked before the introduction of the change, and would stop working or work incorrectly after it. Interactive use-cases like the quality of syntax highlighting are important (of course), but I think that it would be a bit disingenuous to call this a “compatibility breaking” issue. For example, the new-ish match-case syntax also initially broke highlighting in some IDEs, but this was of course quickly fixed.

Sure, the missing highlighting and link might be a minor inconvenience for some developers during the transition period, but it wouldn’t influence any production code, and this is only a convenience issue vs a “breaks my workflow” issue since the developer can always fall back to scrolling to the desired line.

In my eyes, this is a trade-off between “making warnings better for everyone” and “making warnings slightly worse for some IDE users (temporarily)”.

Btw, what does your IDE currently do for python exceptions? My terminal correctly adds links to both formats ({filename}:{lineno} and File "{filename}", line {lineno}). Any IDEs which currently support Python exceptions would automatically support the new warning format.

Perhaps using colour could help? It would need to detect tty to avoid breaking piped output. Compare recent work in pip:

I am not sure, what are you proposing here. Unless Python starts shipping with a vendored version of rich, I don’t see how this would work. My understanding is that this kind of terminal-specific magic is really fragile and hard to get right across platforms. (but I am not an expert, feel free to correct me if I am wrong)

Tools (like VSCode or emacs) detect the filename:lineno pattern in general, not necessarily in Python-specific components, so a different syntax might never get supported.

I’ve thrown together a quick proof of concept implementation (in pure python).

As I’ve mentioned previously, I am 100% open to exploring alternative formats. As long as we agree, that there is room for improvement from the way Python currently formats warnings.

For example, what do you think about

File "/path/to/file.py:XX", in foo
  bar(deprecated_baz().boo())
      ^^^^^^^^^^^^^^^^
  DeprecationWarning: You shouldn't do that, use wow.blam() instead

Also, if this is really such a common use case, why don’t Exceptions use this format? I am genuinely curious, if there was a good reason, why python tracebacks didn’t follow the common pattern. And if there isn’t a good reason, maybe we should change the Exception format to follow the same pattern too!

Edit: It appears that the current Exception format predates even Python 2.0, so there may not be a good reason for why it’s File "foo.py", line XX instead of foo.py:XX.

2 Likes

I think the proposal would be a nice improvement. The filename:lineno format doesn’t matter that much to me personally. I think a compromise like the above, which preserves the : could be a great idea if it helps people that rely on it, and allows to move this forward. :+1:

I feel that the current format is fine and “fixing” it will just cause churn. Let’s move on to something else.

1 Like

I can understand not wanting to waste time on minor issues like this, but can we at least hide the

  warnings.warn(

source line, when stacklevel isn’t explicitly set in a warnings.warn call? The line noise is really triggering my OCD. :laughing:

This should be a pretty simple change, probably without any negative side effects.

Jokes about medical problems can sometimes rub people the wrong way and are best avoided.

I do like the idea of cleaning up the warnings stack output a bit. It’s often hard to tell what actually triggered a warning, even when the library sets stacklevel appropriately. If we could get output similar to the new tracebacks with location range indicators, that would be really helpful. I don’t think that requires sacrificing the general line format.

2 Likes