`@no_type_check` decorator

The current typing spec includes a short description of the @no_type_check decorator here. The wording was copied directly from PEP 484. It’s short, so I’ll copy it verbatim here:

To mark portions of the program that should not be covered by type hinting, you can use the @typing.no_type_check decorator on a class or function. Functions with this decorator should be treated as having no annotations.

There are multiple issues with the current definition.

Issue 1: Applying @no_type_check to classes
The spec indicates that @no_type_check can be applied to a class, but it doesn’t specify what behavior is implied in this case. Should all type errors within the class definition be suppressed? Should the entire class be treated as Any? Should any class derived from this class be treated as Any? Should the decorator automatically apply to nested classes, methods and functions within the class?

No type checker (including mypy, which was the reference implementation for PEP 484) has ever implemented support for @no_type_check applied to a class. It is simply ignored by all four major type checkers in this context.

Issue 2: Applying @no_type_check to functions
Mypy (and only mypy, among the four major type checkers) does implement support for @no_type_check when applied to a function.

The spec is ambiguous about what behavior this should entail, but it does hint at the intended behavior.

The problem is that the phrase “treated as having no annotations” has different meanings to different type checkers (and even different modes within type checkers). For example, mypy defaults to suppressing all errors in unannotated functions, but this can be overridden with the check_untyped_defs option. The other three major type checkers report errors by default in unannotated functions.

My best guess is that the authors intended for the spec to mean:

  1. All type-related errors within the function should be suppressed, including any errors related to its declaration, body, or any other decorators applied to it.
  2. If the function’s signature includes type annotations for any of its parameter or its return type, these should be ignored and assumed to be Any when evaluating the type of the function symbol. This would presumably also apply to the signature of the post-decorated function if decorators were applied on top of @no_type_check.

If that interpretation is correct, I would expect to see the following behavior:

@no_type_check
def func(a: int, b: int) -> int:
    return a + b + ""  # No error

func("", "")  # No error
func()  # Error
func(other=1)  # Error

Mypy does not match the behavior. Instead, it appears to replace the entire signature with Callable[..., Any] and allow any arguments to be passed. This seems counter to the text description in the spec, and it’s not clear to me why this behavior would be desired.

Questions for the Community

  • Do any of you use @no_type_check in your code base? If so, why?
  • Are there any concerns about deprecating @no_type_check?

Commentary
I’d like to explore the idea of deprecating @no_type_check.

I think the main motivation for originally adding support for @no_type_check was to support alternative uses for the annotation syntax. The type system has evolved considerably since these early days, and Annotated (which was added in PEP 593) is now the recommended way to handle non-type-related annotations.

Because of this, I don’t see any remaining utility for @no_type_check. If we do identify some need for it, we should then discuss how to better define it because the current spec is too ambiguous.

Based on my experience as maintainer of pyright, here are my recommendations in order or preference:

  1. Deprecate @no_type_check. Conformant type checkers are allowed to ignore it (and/or flag it as deprecated) regardless of whether it’s applied to functions or classes. The typing.no_type_check symbol is slated for removal in a future release of Python.
  2. Deprecate @no_type_check for classes; type checkers are allowed to ignore it in this case. When applied to a function, @no_type_check indicates that all type-related errors within that function are suppressed. From the perspective of a caller, all of the function’s parameters and return type are assumed to be implicitly Any even if they are otherwise annotated or the type checker would normally infer the return type. Non-type errors (like syntax errors or conditions that will always result in a runtime exception) can still be reported.

Mypy has had an issue open for supporting @no_type_check on classes since 2015 with very little interest (one upvote, one abandoned PR). I think it’s fine to remove this feature from the spec.

However, @no_type_check on functions does get some use: Code search results · GitHub. I glanced at a few use cases and it mostly seems to be used as a function-scoped type ignore; users want to suppress all typing in a particular function. I think it’s better to use # type: ignore in that case, which is more narrowly scoped, supports specifying an error code, and can be recognized as unused (with --warn-unused-ignores in mypy). However, the feature has been around for a long time and is in use, so I’d be hesitant to deprecate it completely.

1 Like

Yeah, I think this decorator was based on a pretty naive idea of what type checkers would do. (It was not present in the prototype mypy implementation that Jukka had been working on for some time when PEP 484 was written.) When it was implemented (for functions only) only the bare minumum was done. The earliest mypy test cases I can find for it don’t test how the resulting function can be called, but slightly newer tests do verify that it can be called with an incorrect number of argument.

I agree that we shouldn’t bother with it for classes given that there’s clearly so little interest in that.

I first thought that maybe we could just remove it from the spec and make it a mypy feature, leaving it up to mypy to decide if and when to deprecate it. The problem with that is that current usage expects it to exist in the stdlib typing module at runtime. So now I’m not sure what to do. Maybe keep typing.no_type_check at runtime but remove it from the spec, and in 5 years time see if it’s still needed?

I would be in favour of your second option. We should definitely remove @no_type_check for classes from the spec; no one has ever used it for this.

I don’t think we should deprecate @no_type_check entirely. It’s somewhat widely used; I see two dozen uses of it in my work codebase. As far as I can tell usually as a function-scoped type ignore, as Jelle mentions, but I’m sure there are folks using non-standard annotations too.

So I’m opposed to removing it from the runtime. We could have it as a mypy only feature, but given that it’s in typing.py, I’d prefer it to be part of the spec. I think as long as we preserve the “function scoped type ignore” use case, we have some latitude in how it is specified, and your recommendation seems good to me.

2 Likes

Why the latter? I’d say if a decorator applied after @no_type_check specifies a given return type, that should be honored – just as if it was applied to a function without any type annotations.

(I have no strong intuition what should happen if @no_type_check is applied to a function that’s already decorated. I guess it could also suppress errors resulting from applying that decorator.)

Agreed on syntax errors; but the other kind seems a bit questionable – there are plenty cases where a guaranteed runtime error is intended (e.g. tests or code to be written later). While the decorator is named @no_type_check, my intuition is more that it means “no errors from the static [type] checker” – even if that checker reports other errors that can be detected statically.

I agree with @hauntsaninja. I view no_type_check as a somewhat fringe part of today’s type system, so I think it’s fine if pyright, or any other type checker, decides they don’t want to support it. But I don’t see a strong case for deprecating it — it’s reasonably popular among mypy users, and deprecating it implies that we’d eventually want to remove it from the runtime typing module. That feels unnecessarily disruptive to me.

So I think we should remove it from the spec, and clearly state in the documentation for CPython’s module that it might not be supported by all type checkers due to it having been removed from the spec. But I’d do no more than that.

Since it would no longer be part of the spec, it would be up to mypy, and/or any other type checkers that opt into supporting it, to decide whether or not to support applying the decorator to classes, and what the semantics there should be.

2 Likes

Yeah, the CPython docs still allude to applying it to classes. I think nobody expects that to ever work even in mypy. Maybe that can be officially deprecated (since surely some folks have mistakenly used it)?

1 Like

Instinctively I like that idea, but what would “deprecating applying it to classes” mean, specifically? Would we:

  1. Just document that you’re not supposed to do it?
  2. Add a deprecation warning at runtime if it was applied to a class?
  3. State in the spec that type checkers should emit a diagnostic if they saw it being applied to a class?
  4. Some combination of the above?

FWIW, I think I vote for (4): I might do all of the above

We might need to issue a deprecation warning at runtime when it’s used on a class – there is currently a bunch of code that tries to set __no_type_check__ on every method. I think it may even recurse into nested classes. There are test for all this too.

I’m not sure if this is an indication that someone is using it at runtime, or whether it just means that we wrote thorough tests. Maybe @sobolevn can help us understand? See Issue 46571: Strange `@typing.no_type_check` behavior for class variables - Python tracker for some background. (One detailed comment there reports that beartype uses it, but only for functions.)

The last time I worked on it, we had explicit tests and implicit convention that @no_type_check affects all things in a class at runtime. So, it recurses into everything it possibly can.

I think that deprecating it might have a negative effect on runtime type checkers. What other options do they have?

Ah, I hadn’t thought about the use case for runtime type checkers. It makes sense that they would have a use for @no_type_check applied to a class.

Perhaps the right answer then is to leave @no_type_check in the typing spec but indicate that both static and runtime type checkers can decide how to interpret its meaning, and they are also free to ignore it.

It’s a symbol in the standard library, so I still think we should specify the behaviour for static type checkers that gets used in practice.

Concretely, we could change the spec like so: Change specification of `@no_type_check` by hauntsaninja · Pull Request #1584 · python/typing · GitHub

And at a minimum update the CPython documentation like so: Align documentation of `@no_type_check` with spec by hauntsaninja · Pull Request #114068 · python/cpython · GitHub

I’m open to loosening the proposed spec change further re: how callers treat it, but I feel we should preserve the “static type checkers should suppress errors within the function” piece.

I searched through Code search results · GitHub and it seems like it’s not used very much as a class decorator / in cases where it is used folks often then also decorate all the methods. I didn’t check exhaustively but I couldn’t find much evidence of runtime type checking in these class decorator uses (based on cross referencing projects that use __no_type_check__ Code search results · GitHub ).

Based on this, I think it would be feasible to DeprecationWarning on class decorator usage (and eventually error). But I’m also content to avoid touching the runtime and just document the status quo, as above.

OK, that works for me.

Here’s another proposal. How about treating @no_type_check essentially as a full off-switch for type-checking? That is:

When a function or class is decorated with @no_type_check, type checkers should report no errors in the definition and treat the decorated object as having type Any.

So if you write:

@no_type_check
def f(...):
  ...

then a type checker reports no errors from the start of the @no_type_check decoration to the end of the body of f, and for checking the rest of the program, the type-checker acts as if it saw f: Any. Same for a class.

I feel like this lines up pretty well with what I might naively expect a decorator called @no_type_check to do. Plus it seems simpler than selectively deprecating some usages, and behavior in some of the edge cases brought up so far becomes fairly straightforward to reason about:

  • Anything that appears in the line range from the start of @no_type_check to the end of the function/class definition, whether that’s another decorator, a nested class, etc., is ignored.
  • Inheriting from a class decorated with @no_type_check is treated the same as inheriting from an object with type Any.
  • If another decorator appears before @no_type_check, the type checker acts as if the decorator were applied to an object with type Any. That is:
@other_decorator
@no_type_check
def f(...):
  ...

is treated like:

f: Any
f = other_decorator(f)

That was definitely the original intention, and probably how most people who use it assume it works (since, for functions, it mostly does that).

The rest of your proposal goes further though – changing the type of the decorated function to Any is more than turning off type checking for the scope of the decorator, it changes things for callers too. (So does mypy’s "change it to Callable[..., Any], but less extreme – it’s still callable.)

Yeah, by full off-switch, I do mean off for callers as well. It certainly is an extreme interpretation of @no_type_check, but as a user, I think it would make more sense to me than @no_type_check meaning that some subset of the errors that a type checker would report when the function is called are disabled.

I’d also be fine with saying the decorated object has type Callable[..., Any] rather than Any. In fact, if that’s what mypy currently does (I wasn’t sure if that’s actually how mypy implements @no_type_check or just a description of what the observable behavior looks like), then maybe the spec can be changed to say that rather than the whole thing about treating the function as having no type annotations, which it sounds like no one has implemented anyway.

I think what I intuitively would expect (and what I probably meant when writing PEP 484) is that

@no_type_check
def foo(a: 42, b: '6*7', *args: 3.14) -> 2+2:
    ...

should be equivalent to

def foo(a, b, *args):
    ...

i.e. just remove the annotations.

An issue here may be that occasionally some type checkers still produce errors about things in the body of such functions? That’s not what PEP 484 intended though.

I would be okay with complaints about bad calls based on the latter interpretation (e.g. foo(1) should give an error IMO), but mypy’s Callable[..., Any] (which I confirmed by looking at the tests) would silence that error as well. I think we need to consider that separately if we decide not to deprecate @no_type_check for functions (and it looks like we won’t).

It does sound like we have different ideas about what a decorator called @no_type_check would intuitively do. My first thought when seeing something called @no_type_check is that it should make the type checker be quiet, so I’d be surprised if I marked something as “type checker, be quiet,” used it, and started getting errors from the type checker. But I might be in the minority here, and I don’t feel strongly about my proposed interpretation =)

What I do feel more strongly about is: I don’t love the idea of deprecating @no_type_check only for classes. Whether you go with a silence all errors or a remove type annotations interpretation, the behavior isn’t specific to functions, so restricting it to functions feels like adding complexity. Part of the reason we’re considering the deprecation seems to be a lack of a clear specification of the behavior on classes, which is what motivated my above proposal.

It appears @guido and the original spec meant it more as @these_are_not_type_annotations and choose the slightly ambiguous term @no_type_check.

I also agree that deprecating it just for classes seems a bit weird. This reminds me of the recent complaint about changing the spec because it’s simpler for type checkers, without considering usecases. I don’t see harm in having the decorator exists for classes. Type checkers ignoring it by itself is not really a problem until a user comes and complains about it. If those users come, they can be told to implement it themselves, after all it’s being implemented by volunteers. Preferably ofcourse, type checkers wouldn’t ignore it, but create an error that they don’t know how to deal with this decorator. But neutering the spec because currently no type checker implements it doesn’t seem like a good idea.

Independent of that, the spec should be made more specific about what the expected behavior is so that type checkers don’t disagree about behavior they do intentionally implement.

1 Like

I can’t tell whether you’re disagreeing with my “Callable[..., Any] is going too far” or with something Eric said. What you say is what I meant in PEP 484 – but whether the scope of the decorator should include calls to the decorated function seems to be controversial and I don’t think I was thinking about that for PEP 484. Adding # type: ignore to every line of the function would not affect calls – but even the earliest implementation in mypy changed the call signature.

Moreover, skimming GitHub hits for @no_type_check, I got the feeling that actual uses generally did have “proper” type annotations but added the decorator to shut the type checker up about calls to the decorated function in other places – which seems to suggest that Callable[..., Any] is actually at least part of the desired behavior.