[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