You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
TypingError: type checker will emit error with a message specified in typing stubs #1043
The type checker's error message doesn't say "you passed in type=bool and it's wrong". It says "statement is unreachable".
The type checker's error message appears on a different line than where the buggy code is.
Unreachable code warnings are disabled by default in mypy, even with --strict.
The solution I would like: if you declare the return type as TypingError["foo bar"], the type checker will display an error message foo bar. This is similar to how #error works in the C preprocessor.
Thanks for the additional examples. That's really helpful.
If we were to add support for this, I'd prefer not to model it as a return type annotation for a couple of reasons.
First, I want to avoid adding more type annotations that accept string literals as type arguments but do not expect the string to be treated as a quoted type expression. Both Literal and Annotated already break this rule, and type checkers like pyright need to include a bunch of complex and fragile special-case handling for these cases. Let's not add more of these unless there's a really compelling reason to do so.
Second, the return type seems like it should be independent of any special error message that is emitted. In some cases, NoReturn is the correct return type because that overload will always generate an exception. In that case, code after the call should be marked unreachable. Other times, a different return type may be appropriate, but it's still desirable to emit an error message.
I think a decorator is a better option for this particular use case. There have been some good discussions around a standardized decorator for @deprecated. This feels like a very similar use case, and I think we could address both use cases with the same solution/pattern.
Here's a rough idea of what that might look like.
fromtypingimportoverload, deprecated, runtime_error# Use of "runtime_error" decorator, always emits error diagnostic@overload@runtime_error("Calling pow() with a mod of 0 will result in a runtime exception")defpow(base: int, exp: int, mod: Literal[0]) ->NoReturn: ...
@overloaddefpow(base: int, exp: int, mod: int) ->int: ...
@overloaddefpow(base: int, exp: _PositiveInteger, mod: None= ...) ->int: ...
# Use of "deprecated" decorator; this can be presented differently if# desired (e.g. crossed-out text at the call site)@overloaddeffoo(x: int) ->int: ...
@overload@deprecated("Option value of 'old' is deprecated")deffoo(x: int, *, option: Literal["old"]) ->int: ...
@overloaddeffoo(x: int, *, option: str) ->int: ...
Thoughts?
Reacted by James Hilton-Balfe, Akuli, Alex Waygood, Thomas Grainger, Robert Craigie, Joe Young, Martin Fischer, Michael H and Finite State Machine
I like the idea of using a decorator, and also adding a similar @deprecated decorator while we're at it. We could combine the two into just a single decorator, but separating them means that users can disable deprecation warnings without also having to suppress errors for type=bool and the like. IDEs and langservers can also mark the function call as deprecated instead of a generic "warning" or "error".
I don't think @runtime_error is a good name for this. For example, passing type=bool to argparse will not actually error at runtime; it will silently eat up the next argument passed on command line and always return True to the rest of your code. If we don't want to go with @typing_error, shortening it to just @error wouldn't be great either, because it's too likely to conflict with a symbol defined in a stub file. How about @type_checker_error?
And yet another use case: if foo: where foo is an Iterator. Iterators are required to have __iter__ and __next__, but usually their boolean value always evaluates to true so if foo: is pointless.
class Iterator:
...
@always_error("not all iterators can be checked for emptiness with their boolean value, consider using Sequence instead")
def __bool__(self) -> bool: ...
Same goes to Iterable, as it makes no guarantees about boolean value. Arguably Iterator[Foo] | None would be a bit of a false positive, but I think it's quite rare compared to wrongly annotating non-None values as Iterable.
I will start preparing a PEP to propose the @typing_error() decorator (as well as typing support for deprecations). If you're interested in giving early feedback, let me know.
Reacted by Akuli, Mehdi Drissi, Martin Fischer, Alex Waygood and wyattscarpenter
Firstly a bit of bikeshedding: I think @type_error makes more sense than @typing_error. A type error happens when you misuse a type, whereas I would understand "typing error" to refer to erroneous type hints.
Secondly: I think it would be nice to additionally have a @potential_type_error for cases where a Literal type is required but we cannot enforce it in the type stubs because the exact value may be unknown for example:
defopen_db(mode: str):
returnopen('example.db', mode) # potential type error: cannot statically determine that the mode is valid
It would be great if we could annotate overloads such as this one as follows:
# Fallback if mode is not specified@overload@potential_type_error('cannot statically determine that the mode is valid')defopen(
file: _OpenFile,
mode: str,
buffering: int= ...,
encoding: str|None= ...,
errors: str|None= ...,
newline: str|None= ...,
closefd: bool= ...,
opener: _Opener|None= ...,
) ->IO[Any]: ...
The difference between @type_error and @potential_type_error would be that the former should always be reported by static type checkers while the latter should only be reported if the static type checker is run in strict mode.
@JelleZijlstra I'd be happy to provide early feedback for the PEP.
② Change Type-Checkers behavior to raise an error whenever an instance of generic `Never` is created outside an appropriate `try`-`except` block.
@overloaddefdivide(a, b: Literal[0]) ->Never[ZeroDivisionError]:...
@overloaddefdivide(a: int, b: int) ->float: ...
defdivide(a, b) : returna/btry:
divide(5, 0) # ✔ raises a ZeroDivisionError but is wrapped in a correct try-except blockexceptZeroDivisionError:
...
try:
divide(5, 0) # ✘ ValueError does not cover ZeroDivisionErrorexceptValueError:
...
divide(5, 0) # ✘ raises ZeroDivisionError
Advantages compared to @typing_error
Compared to the proposed @typing_error decorator, this would not flag when illegal function calls are wrapped in appropriate try-expect block. Even in a static context, this may be useful, for example when writing a unit test that checks if the appropriate error is raised.
Example with pytest...
importpytestwithpytest.raises(ZeroDivisionError):
divide(5, 0) # <- static type checker can safely ignore this one.
It is more correct from a type-theory standpoint. According to the documentation:
That is, as Wikipedia states, in a sound type-system, the bottom type in uninhabited, meaning that no instances of Never can exist. However, calling a function of type Callable[..., X] always returns an instance of X, and so Callable[..., Never] would create an instance of Never, hence a type-error should be raises when such a function is called.
This matters since instances of Never might be created implicitly in other ways, for example by exhausting a Union:
Example of exhausting a union.
deffoo(x: int|tuple[int, ...]) ->str:
ifisinstance(x, int):
return"an integer"ifisinstance(x, tuple):
return"a tuple"reveal_type(x) # bug: mypy 1.4.1 doesn't even reveal the type.y: int=hash(x) # <- under new paradigm this should raise an error# either [unreachable], or it should complain that `Never` as an argument# is equivalent to passing no argument and hash() yields TypeError
Disadvantages compared to @typing_error
The main disadvantage is that the proposed @typing_error-decorator can be used to pass specific messages directly to the type checker (see #1043 (comment)). However, there is no reason why not both can be used simultaneously. See below for an example that combines both ideas.
Example combining both typing_error and Never[ZeroDivisionError]...
@overload@typing_error("Division by 0 is undefined")defdivide(a, b: Literal[0]) ->Never[ZeroDivisionError]: ...
@overloaddefdivide(a: int, b: int) ->float: ...
defdivide(a, b): returna/btry:
divide(5, 0) # no error raisesexceptZeroDivisionError:
...
try:
divide(5, 0) # type-checker should give 3 messages# 1. [uncaught-exception]: divide raises ZeroDivisionError# 2. Note: "Division by 0 is undefined"# 2. Note: ValueError does not cover ZeroDivisionErrorexceptValueError:
...
divide(5, 0) # type checker should give 2 messages:# 1. [uncaught-exception]: divide raises ZeroDivisionError# 2. Note: "Division by 0 is undefined"
Calling functions that return Never/NoReturn is entirely valid, it's just that you should expect that execution no longer continues after the function. For example sys.exit() is NoReturn, and any other function that unconditionally raises is too. So is something that contains an infinite loop (say an event loop), that would never end. What would the generic type be in that case? There's no exception raised at all.
@TeamSpen210 For a genuine infinite loop, I'd imagine simply non-generic Never, which could be identified with BaseException or Never[KeyboardInterrrupt | SystemExit], as the loop will only terminate on KeyboardInterrupt, SystemExit and the likes. While the type-checker flagging calls to such an event loop might be annoying at first, they could also act as a reminder to wrap the event loop in a try-except or at least try-finally block for cleanup.
My point is that such calls are entirely valid code, where you don't necessarily want to even catch the exception at all. Type checkers flagging the calls would simply be wrong. When you use sys.exit(), you don't want to be catching the exception, that defeats the entire point. Actually, thinking about it, your proposal is just checked exceptions? The same reasoning would make it useful to apply the same behaviour to say open(), to ensure you have a try-except for FileNotFoundError. It's not related to the return type.
Reacted by Alex Waygood, Randolf Scholz, Shantanu and wyattscarpenter
Activity
erictraut commentedon Jan 24, 2022
How common is this use case? Have you seen more than just this one case?
AlexWaygood commentedon Jan 24, 2022
Here are two other places in typeshed where we have
NoReturnoverloads that kinda-sorta-half work, where this feature would be useful:builtins.pow(first overload): https://github.com/python/typeshed/blob/fabd8428572dcc20b7ae7919e16d9b4c87cf3da2/stdlib/builtins.pyi#L1330datetime.date.__sub__(second overload): https://github.com/python/typeshed/blob/fabd8428572dcc20b7ae7919e16d9b4c87cf3da2/stdlib/datetime.pyi#L72I'm sure there are yet more examples, but these two are just off the top of my head.
Akuli commentedon Jan 24, 2022
A couple more examples where unreachable code warnings don't do a good job of explaining what's wrong:
wave.Wave_read.getmarkand a few other wave methods unconditionally raise an error.__delitem__that always raisesTypeError: mmap doesn't support item deletion.erictraut commentedon Jan 24, 2022
Thanks for the additional examples. That's really helpful.
If we were to add support for this, I'd prefer not to model it as a return type annotation for a couple of reasons.
First, I want to avoid adding more type annotations that accept string literals as type arguments but do not expect the string to be treated as a quoted type expression. Both
LiteralandAnnotatedalready break this rule, and type checkers like pyright need to include a bunch of complex and fragile special-case handling for these cases. Let's not add more of these unless there's a really compelling reason to do so.Second, the return type seems like it should be independent of any special error message that is emitted. In some cases,
NoReturnis the correct return type because that overload will always generate an exception. In that case, code after the call should be marked unreachable. Other times, a different return type may be appropriate, but it's still desirable to emit an error message.I think a decorator is a better option for this particular use case. There have been some good discussions around a standardized decorator for
@deprecated. This feels like a very similar use case, and I think we could address both use cases with the same solution/pattern.Here's a rough idea of what that might look like.
Thoughts?
Akuli commentedon Jan 24, 2022
I like the idea of using a decorator, and also adding a similar
@deprecateddecorator while we're at it. We could combine the two into just a single decorator, but separating them means that users can disable deprecation warnings without also having to suppress errors fortype=booland the like. IDEs and langservers can also mark the function call as deprecated instead of a generic "warning" or "error".I don't think
@runtime_erroris a good name for this. For example, passingtype=boolto argparse will not actually error at runtime; it will silently eat up the next argument passed on command line and always returnTrueto the rest of your code. If we don't want to go with@typing_error, shortening it to just@errorwouldn't be great either, because it's too likely to conflict with a symbol defined in a stub file. How about@type_checker_error?AlexWaygood commentedon Jan 24, 2022
I also like the idea of a decorator. How about
@always_errors?graingert commentedon Feb 4, 2022
asyncio's
loopkwarg removal in 3.10 would apply here tooAkuli commentedon Aug 21, 2022
And yet another use case:
if foo:wherefoois anIterator. Iterators are required to have__iter__and__next__, but usually their boolean value always evaluates to true soif foo:is pointless.Same goes to
Iterable, as it makes no guarantees about boolean value. ArguablyIterator[Foo] | Nonewould be a bit of a false positive, but I think it's quite rare compared to wrongly annotating non-None values asIterable.JelleZijlstra commentedon Oct 27, 2022
I will start preparing a PEP to propose the
@typing_error()decorator (as well as typing support for deprecations). If you're interested in giving early feedback, let me know.not-my-profile commentedon Dec 28, 2022
Firstly a bit of bikeshedding: I think
@type_errormakes more sense than@typing_error. A type error happens when you misuse a type, whereas I would understand "typing error" to refer to erroneous type hints.Secondly: I think it would be nice to additionally have a
@potential_type_errorfor cases where aLiteraltype is required but we cannot enforce it in the type stubs because the exact value may be unknown for example:It would be great if we could annotate overloads such as this one as follows:
The difference between
@type_errorand@potential_type_errorwould be that the former should always be reported by static type checkers while the latter should only be reported if the static type checker is run in strict mode.@JelleZijlstra I'd be happy to provide early feedback for the PEP.
randolf-scholz commentedon Aug 5, 2023
An Alternative
I want to propose an idea that is somewhat different to what is discussed here, both can exist somewhat in parallel. It consists of 2 changes:
① Make Never generic to allow more descriptive exceptions (Never[SystemExit], Never[ZeroDivisionError], etc.).
A return hint of
Neveris equivalent to a function always raising an exception. A genericNeverallows us to be more specific:② Change Type-Checkers behavior to raise an error whenever an instance of generic `Never` is created outside an appropriate `try`-`except` block.
Advantages compared to
@typing_errorCompared to the proposed
@typing_errordecorator, this would not flag when illegal function calls are wrapped in appropriate try-expect block. Even in a static context, this may be useful, for example when writing a unit test that checks if the appropriate error is raised.Example with pytest...
It is more correct from a type-theory standpoint. According to the documentation:
That is, as Wikipedia states, in a sound type-system, the bottom type in uninhabited, meaning that no instances of
Nevercan exist. However, calling a function of typeCallable[..., X]always returns an instance ofX, and soCallable[..., Never]would create an instance ofNever, hence a type-error should be raises when such a function is called.This matters since instances of
Nevermight be created implicitly in other ways, for example by exhausting aUnion:Example of exhausting a union.
Disadvantages compared to
@typing_errorThe main disadvantage is that the proposed
@typing_error-decorator can be used to pass specific messages directly to the type checker (see #1043 (comment)). However, there is no reason why not both can be used simultaneously. See below for an example that combines both ideas.Example combining both typing_error and Never[ZeroDivisionError]...
TeamSpen210 commentedon Aug 5, 2023
Calling functions that return
Never/NoReturnis entirely valid, it's just that you should expect that execution no longer continues after the function. For examplesys.exit()isNoReturn, and any other function that unconditionally raises is too. So is something that contains an infinite loop (say an event loop), that would never end. What would the generic type be in that case? There's no exception raised at all.randolf-scholz commentedon Aug 5, 2023
@TeamSpen210 For a genuine infinite loop, I'd imagine simply non-generic
Never, which could be identified withBaseExceptionorNever[KeyboardInterrrupt | SystemExit], as the loop will only terminate onKeyboardInterrupt,SystemExitand the likes. While the type-checker flagging calls to such an event loop might be annoying at first, they could also act as a reminder to wrap the event loop in atry-exceptor at leasttry-finallyblock for cleanup.TeamSpen210 commentedon Aug 5, 2023
My point is that such calls are entirely valid code, where you don't necessarily want to even catch the exception at all. Type checkers flagging the calls would simply be wrong. When you use
sys.exit(), you don't want to be catching the exception, that defeats the entire point. Actually, thinking about it, your proposal is just checked exceptions? The same reasoning would make it useful to apply the same behaviour to sayopen(), to ensure you have a try-except forFileNotFoundError. It's not related to the return type.randolf-scholz commentedon Aug 5, 2023
@TeamSpen210 You're right, it would make more sense if only generic -
Neverwould be flagged.stefanv commentedon Aug 20, 2025
Another example use-case of
@type_erroris currently being discussed at https://discuss.python.org/t/type-checking-only-typed-input/102570/17