[Python-ideas] Type hints for functions with side-effects and for functions raising exceptions

Steven D'Aprano steve at pearwood.info
Tue Feb 19 18:12:48 EST 2019


On Tue, Feb 19, 2019 at 11:05:55PM +0200, Miikka Salminen wrote:
>  Hi!
> 
> To help automatic document generators and static type checkers reason more
> about the code, the possible side-effects and raised exceptions could also
> be annotated in a standardized way.

This is Python. Nearly everything could have side-effects or raise 
exceptions *wink*

Despite the wink, I am actually serious. I fear that this is not 
compatible with duck-typing. Take this simple example:

@raises(TypeError)
def maximum(values:Iterable)->Any:
    it = iter(values)
    try:
        biggest = next(it)
    except StopIteration:
        return None
    for x in it:
        if x > biggest:
            biggest = x
    return x

But in fact this code can raise anything, or have side-effects, since it 
calls x.__gt__ and that method could do anything.

So our decoration about raising TypeError is misleading if you read it 
as "this is the only exception the function can raise". You should read 
it as "this promises to sometimes raise TypeError, but could raise any 
other exception as well".

This pretty much shows that the idea of checked exceptions doesn't go 
well with Python's duck-typing.

And I note that in Java, where the idea of checked exceptions 
originated, it turned out to be full of problems and a very bad idea.


Here is your example:

> In [3]: @raises(ValueError)
>    ...: def hello_if_5(x: int) -> None:
>    ...:     if x != 5:
>    ...:         raise ValueError("number other than 5 given")
>    ...:     print("Hello!")

In this *trivial* function, we can reason that there are no other 
possible exceptions (unless ValueError or print are monkey-patched or 
shadowed). But how many of your functions are really that simple? I 
would expect very few.

For the vast majority of cases, any time you decorate a non-trivial 
function with "raises(X)", it needs to be read as "can raise X, or any 
other exception".

And similarly with annotating functions for side-effects. I expect that 
most functions need to be read as "may have side-effects" even if 
annotated as side-effect free.

So the promises made will nearly always be incredibly weak:

- raises(A) means "may raise A, or any other unexpected exception"
- sideeffects(True) means "may have expected side-effects"
- sideeffects(False) means "may have unexpected side-effects"

I don't think there is much value in a static checker trying to reason 
about either. (And the experience of Java tells us that checking 
exceptions is a bad idea even when enforced by the compiler.) If I'm 
right, then adding support for this to the std lib is unnecessary.

But I could be wrong, and I encourage static checkers to experiment. To 
do so, they don't need support from the std lib. They can provide their 
own decorator, or use a comment:

    # raises ValueError, TypeError
    # +sideeffects
    def spam(): 
        ...



-- 
Steve


More information about the Python-ideas mailing list