Sunday, March 14, 2021

Friendly version 0.3 has been released

Friendly version 0.3 has been released. This version also marks the official name change from the former friendly-traceback.

Before I started working on Friendly, I assumed that to do custom exception handling, one simply had to redefine sys.excepthook. However, I since found out that this does not work in many environments, including all those based on IPython (which include Jupyter notebooks). Even Python's IDLE, at least up until Python version 3.10.0a5, does not work with a simple replacement of sys.excepthook.

Additionally, creating coloured output varies depending on the programming environment. Thankfully, this can be done in most environments simply by using Rich -- sometimes supplemented by a bit of additional code.

As a result, friendly includes various custom modules so that it can work with the following:

Friendly has now over 200 test cases. Most of these are not unit tests, but instead are integration tests: given code X that raises exception Y (including message Z), ensure that the output is as expected.  For example, suppose that after module M is imported, we try to use attribute A containing a typo. Such a base scenario might need 2 test cases to over all possibilities: whether we can find an attribute that is a similar word, likely indicating a typo, or not.  Here's an example of what one can get in the first scenario:

>>> import math
>>> a = math.sine(1)

Traceback (most recent call last):
File "<friendly-console:2>", line 1, in <module>
a = math.sine(1)
AttributeError: module 'math' has no attribute 'sine'

Did you mean one of the following: `sin, sinh, asin`?

Friendly included a "hint" (Did you mean one of the...). Further information can be obtained by using what(), why(), where(), etc., as described in previous posts.

[Note to self: for the above example, the hint should only include the most likely candidate, namely 'sin', leaving the other choices for the longer explanation done with "why()", as was done for other type of exceptions.]

Currently, about half of the test cases are examples of SyntaxError. I suspect that this will continue to be the case as work on friendly continues and the total number of test cases continues to grow. I would not be surprised if the total number of test cases were to double by the time version 1.0 is ready.  My goal for version 1.0 is to try to cover all possible exception cases that can occur when making mistakes while trying to use code from Python's standard library.

Saturday, March 06, 2021

Going back in history

Imagine that you wish to run a program that takes a long time to run. Just in case somethings goes wrong, you decide to use friendly-traceback (soon to be renamed...) in interactive mode to run it.  This turns out to be a good decision:

Time to explore what might be the problem, and where exactly things might have gone wrong.

Ooops ... a silly typo. Easy enough to correct:

Unfortunately, that did not work: Friendly-traceback, like all Python programs designed to handle exceptions, only capture the last one.

This has happened to me so many times; granted, it was always with short programs so that I could easily recreate the original exception. However, I can only imagine how frustrating it might be for beginners encountering this situation.

A solution


Fortunately, you are using the latest version of Friendly-traceback, the one that records exceptions that were captured, and allows you to discard the last one recorded (rinse, and repeat as often as needed), thus going back in history.



Now that we are set, we can explore further to determine what might have gone wrong.




Friday, March 05, 2021

Saturday, February 27, 2021

Friendly-traceback: testing with Real Python

Real Python is an excellent learning resource for beginning and intermediate Python programmers that want to learn more about various Python related topics. Most of the resources of RealPython are behind a paywall, but there are many articles available for free. One of the free articles, Invalid Syntax in Python: Common Reasons for SyntaxError, is a good overview of possible causes of syntax errors when using Python. The Real Python article shows code raising exceptions due to syntax errors and provides some explanation for each case.

In this blog post, I reproduce the cases covered in the Real Python article and show the information provided by Friendly-traceback. Ideally, you should read this blog post side by side with the Real Python article, as I mostly focus on showing screen captures, with very little added explanation or background.

If you want to follow along using Friendly-traceback, make sure that you use version 0.2.34 or newer.

Missing comma: first example from the article

The article starts off showing some code leading to this rather terse and uninformative traceback.


Since the code is found in a file, we use python -m friendly_traceback theofficefacts.py to run it and obtain the following.


Misusing the Assignment Operator (=)

We only show one example here, as the others mentioned in the article would be redundant. We remind you for one last time that, if you are not doing so, you should really look at the Real Python article at the same time as you go through this rather terse blog post.


Friendly traceback provides a "hint" right after the traceback. We can get more information by asking why().


Misspelling, Missing, or Misusing Python Keywords

Identifying misspelled keywords was actually inspired by that article from Real Python.



Note that Friendly-traceback identifies "for" as being the most likely misspelled keyword, but gives other possible valid choices.

Friendly-traceback can also identify when break (and return) are used outside a loop.



To the English reader, Friendly-traceback might seem to add very little useful information. However, keep in mind that all this additional information can be translated. If you read the following and do not understand what "boucle" means, then you might get an idea of the some of the challenges faced by non-English speakers when using Python.


In some other cases, like the example given in the Real Python article, Friendly-traceback can identify a missing keyword.


As long as there is only one instance of "in" missing, Friendly-traceback can identify it properly.


Finally, two more cases where a Python keyword is not used properly.



Missing Parentheses, Brackets, and Quotes

Five examples taken from the Real Python article offered without additional comments.






Mistaking Dictionary Syntax


Using the Wrong Indentation

Real Python gives many examples. They would all be handled correctly by Friendly-traceback in a similar way as the single example we decided to use for this post.



Defining and Calling Functions




Changing Python Versions



Friendly-traceback requires Python version 3.6 or newer. Not shown here is that it can recognize that the walrus operator, :=, is not valid before Python version 3.8 and give an appropriate message.


Last example: TypeError result of a syntax error.

Let's look at the last example in the Real Python article.


The explanation given by Friendly-traceback might seem weird "the object (1, 2) was meant to be a function ...".  Often one might have assigned a name to that object, which leads to an explanation that should be seen as more reasonable.




The explanation of looking for a "missing comma" when this TypeError is raised was actually added following a suggestion by S. de Menten in the recent contest I held for Friendly-traceback.

There is more ...

Friendly-traceback includes many more cases that those shown above and mentioned in the Real Python article. However, it is limited in that it can only identify the cause of syntax errors there is a single word or symbol used incorrectly or if the error message provided by Python is more informative than the dreaded SyntaxError: invalid syntax.

Thursday, February 18, 2021

My wish for Python 4

I love Python. 

A few years after I started using it, I saw someone writing about it and using the phrase "it fits my brain": this is very much how I feel ... at least, for simple straightforward code that doesn't rely on weird metaclass constructs, or even with the added distraction of type annotations. [Yes, type annotations can be extremely useful, but they do not (currently) "fit my brain".]

I am extremely grateful to the many volunteers that work constantly to improve Python. Thanks to their efforts, Python keeps growing. I see most of the growth as positives: more users, more applications in a growing number of fields. In many universities, Python has displaced languages such as Scheme and (thankfully!) Java as the first language that students learn.  From *my* limited point of view, there is a small negative in that this growth of Python includes a growth in its syntax: when I first encountered Python (version 2.3), it had a comparatively very simple syntax which meant that it was easier to learn (in spite of some warts that were fixed in the 2 to 3 transition) compared with the latest Python version (3.10). 

I am well aware that Python doesn't use semantic version numbers: code written for version 3.x can be incompatible with code written for version 3.y. [This was also the case for the 2.x series.] As a decision has been made to use two-digit minor version numbers, there is no apparent need to think of a version 4 of Python: improvements can continue for many years while keeping 3 as the main version.  However, I wish there could be a version 4 - as I describe below.

Note that when I think of Python, I do not think of a specific implementation (such as CPython), but I think of Python as the language. I do not consider "implementation details" such as the Global Interpreter Lock (GIL) as part of the language. Yes, it would be useful if the main Python implementation could make better use of multi-core CPUs. However, that is not something that is relevant for the purpose of this post.

So, what is my wish for Python 4? ...  

Above all, going from the last 3.x version (let's call it 3.14...) to 4.0 should be done seamlessly: code written for version 3.14 should run as is in Python 4.0.

I would like for Python 4 to get inspired by Racket and introduce "dialects".  Python could even borrow the notation used by Racket (#lang dialect) as a top directive in a given module to specify the dialect used in that module.  Unlike Racket, I would limit the number of possible dialects to 4.

The main dialect would not need to be specified: it would simply be the standard Python that everyone knows and loves (or not). It would continue evolving, changing slightly as it goes from version 4.x to 4.y.

A second dialect would be an "experimental" dialect. This dialect could be use to introduce some new syntax with no guarantee whatsoever of backward (or forward) compatibility. It would allow people to experiment with proposed new syntactic constructs before deciding to incorporate them (or not) in the main dialect. I honestly think that this would help reduce some friction in the Python community as changes are proposed and adopted.  The main benefit of such a dialect would be more social than technical.

A third dialect would be a "beginner" dialect. The goal of the beginner dialect would be to make it easier to learn basic programming concepts as opposed to learning a quirky syntax to express these concepts. This beginner dialect would be, in version 4.0, a strict subset of the main dialect. It would not include type annotations, and it might perhaps also exclude the new pattern matching syntax and other syntactic constructs.  For example, using the keyword is might only be limited to checking if the object is one of the three singletons (None, True, False); other uses of is could and should be done with "== id(thing)". Based on feedback from educators, it might perhaps make sense to eventually introduce a few additional keywords and constructs not available from the main dialect, such as:

  • Having nobreak as a keyword equivalent to else in loops.
  • Having function as a keyword equivalent to lambda.
  • Having imported as a keyword with a meaning equivalent to not __name__ == "__main__".
  • Having repeat as a keyword  as is available in TygerJython and Reeborg's World for the construct repeat nb_steps:. It might perhaps be also useful to have repeat forever: as equivalent to Python's while True: . [Other possible uses of this keyword are described here.]
Finally, a fourth dialect would be a "static" dialect. This fourth dialect would always be using a syntax strictly compatible with the main dialect. In this dialect, some dynamical features of Python (such as the possibility to change the type of the object specified by a given name) would not be available so that some optimizations could be applied to increase the execution speed.  I am sure that experts would be able to suggest other restrictions that could be used to greatly increase the execution speed.  I think that such a dialect would be one that would generate the most enthusiastic response from Python users.

That being said, I doubt very much that I'll ever see Python adopting these ideas. However, it is sometimes nice to dream ...

Thursday, February 11, 2021

Friendly-traceback's www function

Today, I saw some write up that Friendly-traceback was discussed on the PythonBytes podcast. A comment made during that podcast suggested that it would be useful if an internet search could be performed, perhaps using a function named www. (Another name was mentioned). So, of course I immediately created an issue ... and implemented a first version of this function.


Thursday, February 04, 2021

Python's tug of war between beginner-friendly features and support for advanced users

Python is my favourite programming language. Since I discovered it in 2004, programming in Python became my favourite hobby. I've tried to learn a few other languages and have never found one as friendly to beginners as Python. As readers of this blog know, these days I am particularly interested in tracebacks, and I am likely paying more attention than most to Python's improvements in this area: Python is becoming more and more precise in the information that it provides to the users when something goes wrong. For example, consider these 2 examples from Python 3.7

Given the message when we try to assign a value to None, we might have expected to see the same when trying to assign a value to the keyword "pass"; instead we get a not so useful "invalid syntax". Of course, if you've been reading this blog before, you won't be surprised that Friendly-traceback can provide a bit more useful information in this case.


However, this is not the point of this post...  Let's see what kind of information Python 3.8 gives us for the first case.


As you can see, it is much more precise: this is a definite improvement.

Let's have a look at another case, using Python 3.8 again:


Again, the dreaded "invalid syntax".  However, this has been significantly improve with the latest Python version, released yesterday.


Again, much better error messages which will be so much more useful for beginners that do not use Friendly-traceback [ even though they should! ;-) ]

There has been a few other similar improvements in the latest release ... but this one example should suffice to illustrate the work done to make Python even friendlier to beginners.  However, this is unfortunately not the whole story.

To make Python useful to advanced users having to deal with large code base, Python has introduced "optional" type annotations. This is certainly something that the vast majority of professional programmers find useful - unlike hobbyists like me.  Let me illustrate this by an example inspired from a Twitter post I saw today.  First, I'll use Python 3.8:


If you know Python and are not actively using type annotations, you likely will not be surprised by the above.  Now, what happens if we try to do the same thing with Python 3.9+



No exceptions are raised! Imagine you are a beginner having written the above code: you would certainly not expect an error then when doing the following immediately after:


Unfortunately, Friendly-traceback cannot (yet!) provide any help with this.



EDIT: this might be even more confusing.

/EDIT

Eventually, I'll make use of the following to provide some potentially useful information.


Ideally, I would really, really like if it were possible to have truly "optional" type annotation, and a way to turn them off (and make their use generate an exception). Alas, I gather that this will never be the case, which I find most unfortunate.

Monday, January 25, 2021

Friendly-contest: we have a winner!

 Since my last post, no new issue has been filed. As the deadline has passed (8 am, AST), I have written a short program to randomly draw a winner. In my last post, I listed incorrectly the entries which I double-checked prior to writing the program, which I tested a few times before the deadline. 

The program I wrote was not the most efficient, but should be easy to understand: I created a list with one item for each valid contest entries, shuffled it and picked the first item on the list as the possible "winner". Just to ensure that I didn't make any silly mistake, I did 100,000 random draws and compared with the original distribution.

The very last of these random draws was determined to be the winner.

Here's the program:

from random import shuffle

entries = {
    "Dominik1123": 19,
    "sdementen": 6,
    "gdementen": 3,
    "tomerv": 5,
    "dcambie": 3,
    "carreau": 1,
}
results = {
    "Dominik1123": 0,
    "sdementen": 0,
    "gdementen": 0,
    "tomerv": 0,
    "dcambie": 0,
    "carreau": 0,
}

tickets = []
for name in entries:
    for number in range(entries[name]):
        tickets.append(name)

nb_trials = 100_000
rescale = len(tickets) / nb_trials

for i in range(nb_trials):
    shuffle(tickets)
    results[tickets[0]] += 1

for name in results:
    results[name] *= rescale


print("entries:", entries)
print("draws  :", results)
print("The winner is:", tickets[0])


And the winner is Dominik1123.


Thanks to every one who filed an issue for the contest, or simply tried Friendly-traceback.

Sunday, January 24, 2021

Friendly-contest: 20 hours left

There is only 20 hours left in the Friendly-traceback contest: write bad code to win a prize. After a slow start, there has been quite a few submissions lately which will definitely help to improve Friendly-traceback. Some submissions included references to StackOverflow questions and were thus determined to be worth two entries.  Currently, the number of contest entries stands as follows (using Github usernames):

  • Dominik1123: 19
  • sdementen: 6
  • gdementen: 3
  • tomerv: 3
  • dcambie: 2
  • carreau: 1

The draw will be made randomly so that, while people having more entries have a better chance of winning, anyone with a valid contest entry could win.


Saturday, January 23, 2021

Friendly contest: two days left after a surge of submissions

This is just a quick update.

Yesterday, the number of valid entries jumped from 9 to 23. Many of them have given me ideas on how to make Friendly-traceback better at finding the cause of the error but a few of them are likely going to be impossible for Friendly-traceback to evaluate properly and zero in on the exact cause of the exception. 

Friday, January 22, 2021

Contest: 3 submitters, 3x3 entries, 3 days left

 The contest I announced for Friendly-traceback has resulted in a total of  nine entries so far from three different programmers.  Two of the cases submitted have already been fixed in the development version. They also gave me some ideas to explore other possible cases and I found a few additional ones that need to be fixed.

Meanwhile, on Twitter, I saw a few discussions (for example, here and here) about improvements to messages given by the Python parser for SyntaxError cases, and comparisons with Pypy which currently does a better job in some cases in providing more useful error messages. However, as recorded in this issue on the Python tracker, doing this in a useful way can be quite challenging:


Strangely enough, the apparent impossibility of giving useful error messages in some cases reassures me since I couldn't figure out an approach that would always give the right clues. Perhaps I need to worry less and keep at it with my ad-hoc approach, looking for small improvements. Like Voltaire wrote: Le mieux est l'ennemi du bien  (Perfect is the enemy of good).

Monday, January 18, 2021

Don't you want to win a free book?

 At the end of Day 2 of the contest, still only one entry. If this keeps up, by next Monday there will not be a draw for a prize, and we will have a winner by default.


The submission was based on the use of __slots__. In playing around with similar cases, I found an AttributeError message that I had not seen before.  Here's a sample code.

class F:
__slots__ = ["a"]
b = 1

f = F()
f.b = 2

What happens if I execute this code using Friendly-traceback. Normally, there would be an explanation provided below the list of variables. Here we see nothing.



Let's inspect by using the friendly console.





I'll have to take care of this later today. Perhaps you know of other error messages specific to the use of __slots__. If so, and if you are quick enough, you could enter the contest. ;-)

Sunday, January 17, 2021

Friendly contest: the race is on

tl; dr: Python was wrong ;-)


After one day, I've had one valid entry submitted to the contest I announced yesterday; I've also had two other submissions from the same contributor that I deemed to be invalid for the contest. The submissions shared a similar characteristics to a different degree: the information provided by Python in the exception message did not tell the whole story and, taken on its own, might have been considered to be misleading. 

One such cases which I did not considered to be valid for this contest was the error message UnboundLocalError: local variable 'x' referenced before assignment  given for code containing the following

def f():
    x = 1
    del x
    print(x)  # raises the exception

When the exception is raised, there is indeed no trace of variable "x". So while there was an assignment for such a variable before, after deletion it no longer exists. Reading this code, instead of an UnboundLocalError, the exception that should probably be raised at this point is NameError: name 'x' is not defined ; however, Friendly-traceback's role is not to second-guess Python but to explain what an exception means and, whenever possible, give more details using the information available when the exception was raised. I considered this case and another one to be beyond the scope of what Friendly-traceback could accomplish.

The entry that I deemed to be valid was based on the following code:

class F:
    __slots__ = ["a"]

f = F()
f.b = 1

The error message given by Python in this case is AttributeError: 'F' object has no attribute 'b'. While technically correct, the problem is not that this object has no such attribute but that it cannot have such an attribute. This information can be easily obtained when the exception is raised and the information provided by Friendly-traceback now includes the following:

The object f has no attribute named b. 
Note that object f uses __slots__ which prevents the creation of new 
attributes. The following are some of its known attributes: a.

Reminder: the contest is open for 8 more days.




Saturday, January 16, 2021

Write bad code to win a prize

 

Summary

Get a chance of winning a prize by writing code with ONE error that Friendly-traceback cannot properly analyze, in one of three categories:

  • SyntaxError: invalid syntax
  • SyntaxError: some message, where some message is not recognized.
  • Any case of NameError, AttributeError, ModuleNotFoundError, UnboundLocalError, ImportError, IndexError, KeyError that is not recognized or is given an incorrect explanation by Friendly-traceback.

Submitted issues about bugs for Friendly-traceback itself are also considered for this contest.

Links: Github issue

Friendly-traceback documentation

The prize

There will be one prize given drawn randomly from all eligible submissions. The prize consists of one ebook/pbook of your choice with a maximum value of 50 USD (including delivery for pbook) as long as I can order it and have it delivered to you. Alternatively, a donation for that amount to the open source project of your choice if it can be made using Paypal.

The details

Each valid issue will get one entry for the contest. Valid issues contain code that might be expected to be written by a beginner or advanced beginner. It excludes code that uses type annotations as well as the use of async and await keywords.  The code is expected to contain ONE mistake only and not generate secondary exceptions.

The code can be run either using the friendly-console, or running in a Jupyter environment or from an editor as described in the documentation.

For a given valid submission, a bonus entry will be given if a link can be provided to an actual example from a site (such as StackOverflow, /r/python or /r/learnpython, etc.) where a question had been asked prior to this contest.

Exceptions that are not recognized by Friendly-traceback or for which the explanation (in English or French) is wrong or misleading are considered to be valid issues.

Submissions that are considered to be duplicate of previously submitted cases (because they basically have the same cause) will not be considered.

Honor code

I would ask that you do not read the source of Friendly-traceback with the sole intent of finding ways to write code that is designed to lead it to provide incorrect explanations.

End of contest

The contest will end on Monday January 25, at 8 AM Atlantic Standard Time.