Skip to content

Flag for disallowing Any #3470

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ilinum opened this issue May 27, 2017 · 12 comments
Closed

Flag for disallowing Any #3470

ilinum opened this issue May 27, 2017 · 12 comments
Assignees

Comments

@ilinum
Copy link
Collaborator

ilinum commented May 27, 2017

In general, we want to provide the user with a flag to disallow the use of implicit and explicit Any types as well as expressions with type Any, so that they can make sure all of their code is typechecked.

@ddfisher and I did some brainstorming yesterday and this proposal is what we came to.
Here are all the places that Any can appear in, followed by discussion of the current state of the flags and the proposal for a new flag.

Types

Explicit

A user can explicitly specify an Any type in variable definition (x: Any = "hello"), function definition(def foo() → Any: ...), a cast(cast(Any, x)), class definition (class C(Any)), NewType, a TypeVar, NamedTuple, or part of a type(List[Any]).

Implicit

Unimported

Unimported types happen when we try to use a type from an import from a module we don’t analyze. This could happen if we’re using --follow-imports=skip or --ignore-missing-imports.
Consider the following example

    # unanalyzed.py
    class C:
        pass
    # test.py
    from unanalyzed import C
    
    def foo(c: C) -> str:
        return c

If you run mypy test.py --follow-imports=skip, the code typechecks from mypy’s point of view. Because C is unresolved, it implicitly becomes an alias for Any.

Omitted Generics

This case occurs when the user does not explicitly specify a generic parameter for a type. For instance, List in a type position becomes List[Any]. It is easy to forget to specify a generic parameter. This can also happen with e.g. lower-case list from builtins.

We should consider disallowing omitting the generic parameter and maybe forbid using builtin types in favor of types from typing module. The proposal for it is here.

Omitted Function Signature element

When a user does not specify an explicit type for a parameter or a return type, it becomes Any.
For example, the function def foo(x) → None: …
implicitly becomes: def foo(x: Any) → None: …

From Error

Any types can come from an ignored error. For instance:

from typing import Union
T = Union[UhOh]  # type: ignore
x: T
reveal_type(x) # error: Revealed type is 'Any'

Expressions

An expression of type Any can come from any Any types described above.
For instance, x + y gets type Any when x has type Any even if y doesn't.

In real-life code, it would be challenging to achieve no Any expressions in the entire module. Note that if the user is calling an untyped method from a different module, expression can have an Any type, even if all Any types are disallowed in the current module. Still, users may want the ability to disallow expressions of type Any completely in certain circumstances.


Current State

Unimported types

Currently, there is a PR (#3405) that adds a flag to disallow unimported types.

Omitted Generics

There is currently a PR for a flag that would give a warning on omitted generic types. I believe we should at the very least have it on by default. Ideally, we should forbid omitted generics.

Omitted Function

Flag --disallow-untyped-defs is already implemented in #1285. This functionality should be merged into the new flag.

From Error

Currently, there is no flag to disable it.

Expressions

Currently, there is no flag to disable it.


Flag

All of these are very similar in their nature and it would make sense to have one flag to rule them all.
It is generally worth it to provide a user with an option to disable each of the types of Anys.
There are several ways to do it.

Have a different flag for each type

Currently, in order to disable a type of Any, you have to use a separate flag for each type. At the very least we should name the flags with similar names because the options are very closely related to each other.

Have several “umbrella” flags

An option would be to have several flags that would cover different types. For instance, --disallow-unimported-any-types, --disallow-ommited-function-types, --disallow-implicit-any-types, --disallow-explicit-any-types, and --disallow-all-any.
However, having this many flags is cumbersome, especially when they overlap. The documentation and usage would be confusing.

Have One Flag with List of Options

I think the best option is to have one flag that takes a list of arguments.
This could look something like this:
--disallow-any=... where ... can be all, unimported, omitted,error, expr
We should also be able to combine several into one, like this: --disallow-any=unimported,omitted. This combination, for example, would disallow imported types and disallow omitted function types.
If the flag parameters are all + any other flag, we should give an error saying that it’s redundant and linking to documentation.

@gvanrossum
Copy link
Member

gvanrossum commented May 29, 2017

I've fixed a few markup mistakes, hope you don't mind (I didn't catch them all). There are some other issues with the text of your description, e.g.

  • The "explicit" section doesn't make it clear that you could also have Any as part of a type, e.g. List[Any].
  • "An expression of type Any can come from any Any types described above" -- this could be clearer, I suppose you are referring to how x + y gets type Any when x has type Any even if y doesn't.

[EDIT] I also worry that the most draconian version is too strict to be useful:

  • Many typeshed stubs don't have complete function signatures -- we don't want mypy to complain about these even when the user forbids them in their own code, unless the user actually calls one of those functions (e.g. stdlib/3/csv.pyi).
  • Stubs also sometimes use explicit Any to describe a function return type that cannot be expressed in the type system. (E.g. pow(), because returning unions is awkward.) Does the most draconian option then mean that you can't call such functions at all?
  • What about data structures that truly can contain anything (e.g. a module's __dict__), or functions that can truly return anything (e.g. getattr())?

@JelleZijlstra
Copy link
Member

Regarding typeshed, there is already custom logic in generate_unused_ignore_notes in errors.py that excludes typeshed code from --warn-unused-ignore errors. Maybe that logic should be extended to exclude all errors in typeshed code, because for normal users (as opposed to mypy/typeshed developers), it's probably almost never useful to see errors from typeshed. One downside would be that it may become harder to detect when the user has a broken copy of typeshed.

@gvanrossum
Copy link
Member

While it seems attractive to just suppress all errors from typeshed, I'd like to be cautious here.

On the one hand, we should ideally fix strictness errors in typeshed rather than ignore them (unless there are conflicting strictness flags, which so far we have managed to avoid). But while we should never have a typeshed that causes errors with mypy's default flags, there are now so many optional strictness flavors that we usually don't know typeshed has a certain error until some user reports it. We'd lose this if we suppress errors in typeshed by default.

On the other hand, when a new strictness flavor is introduced, we usually can't fix typeshed at the same time (typeshed is big!), so for users who run into this, we should have some way to silence that error in typeshed until we get to it. Maybe we need a separate flag to ignore typeshed errors that users can pass?

(It's not easy to use the per-module config flags for this purpose since most of typeshed is toplevel modules, so there's nothing to put in front of the '*' pattern.)

@ilinum
Copy link
Collaborator Author

ilinum commented May 30, 2017

The "explicit" section doesn't make it clear that you could also have Any as part of a type, e.g. List[Any].

That's a good catch! Yes, this is an explicit Any and should be reported if a flag to disallow explicit Any is enabled. I edited the original proposal to reflect that.

"An expression of type Any can come from any Any types described above" -- this could be clearer, I suppose you are referring to how x + y gets type Any when x has type Any even if y doesn't.

Yep, updated it with your example!

I share your concern about most restrictive flags not being useful. There will always be places where Any is returned.
This is not in the original proposal but what do you think about the following:

If a variable/function of type Any is assigned to a typed variable, this will not give you an error even with all Any expressions disallowed.
For instance, consider the following:

# unchecked.py
def foo(i: int) -> Any:
    if i >= 0:
        return 1
    else:
        return 'negative'
# checked.py
x: int = foo(1)
y: str = foo(-1)

Code in checked.py should typecheck without any errors since we immediately give it a type by assigning it to a variable.

@gvanrossum
Copy link
Member

That example seems designed to undermine the argument since you could also write

x: int = foo(-1)
y: str = foo(1)

I wonder if there shouldn't be a flag that "nerfs" Any by making it similar to object? I.e. assignment to a variable or argument of type Any would be allowed, but assignment of a value of type Any to a variable (or passing to an argument) with a more restrictive type would be an error.

@ilinum
Copy link
Collaborator Author

ilinum commented May 30, 2017

Well, it becomes the responsibility of the caller to determine the type correctly. You could argue it either way.
I would say the person writing the function call has more information and can more accurately tell the type. In this example, the caller would know what is the sign of the integer parameter.

As for making Any behave like object, when would that be useful?

@gvanrossum
Copy link
Member

As for making Any behave like object, when would that be useful?

It would flag all places where Any is used in an unsafe way, i.e. presuming the runtime type matches the compile-time type. It's amazing how often some corner case or a change in some other place of the code makes what was once safe no longer so. (Though in mypy the champion crash cause is probably cast() -- it also assumes the programmer knows what they are doing.)

@ilinum
Copy link
Collaborator Author

ilinum commented May 31, 2017

Hmm, actually, --treat-any-as-object might be better than forbidding Any in a module altogether. As you pointed out earlier, it may be difficult to remove all Anys whereas if Any is treated as object, the user is forced to check the type at runtime using isinstance().
The downside to your proposed approach, is that it either makes the code more verbose or makes people use casts blindly, neither of which is good.

@ilinum
Copy link
Collaborator Author

ilinum commented May 31, 2017

Going back to the original proposal, below is what documentation would look like. A couple of sidenotes:

  • I'm not convinced a flag to forbid Any​ from errors is needed. It seems like it is would only be used together with # type: ignore.
  • unfollowed may not be the best word to describe the imports that are either imports from a module that doesn't exist or when a --follow-imports=skip is set.

Disallow Any Documentation

--disallow-any is a flag to disallow various types of Any in a module. The flag takes a comma-separated list of the following arguments: all, explicit, unimported, untyped, error, expr.

  • explicit disallows usage of explicit Any types in type positions, including type parameters.
  • unimported disallows usage of types that come from unfollowed imports (such types become aliases for Any). Unfollowed imports occur either when the imported module does not exist or when --follow-imports=skip is set.
  • generics disallows usage of generic types that do not specify explicit type parameters.
  • untyped disallows function definitions that are not fully typed (i.e. that are missing an explicit type annotation for any of the parameters or the return type).
  • error disallows usage of Any types that come from ignored errors (using # type: ignore). For instance, if an error happens when a variable is declared, that variable will have type Any.
  • expr disallows all expressions in the module that have type Any.

@JukkaL
Copy link
Collaborator

JukkaL commented May 31, 2017

A few quick notes:

  • Add an example such as list to the documentation for generics.
  • What about an option for just rejecting variables with an inferred (i.e. not annotated) Any type? This was brought up in an earlier discussion. The idea is to require an explicit annotation for all such variables. The name of the option could perhaps be inferred.
  • Another option that was discussed before was a subset of your proposed expr where Any types can only be used 'safely' in expressions. Example usages include as the first argument to isinstance or the second argument to cast or as a function argument when the expected type is Any or object (though the prior may be questionable). Assignments from an Any variable to Any variable would also be okay.

@ilinum
Copy link
Collaborator Author

ilinum commented May 31, 2017

@JukkaL

Add an example such as list to the documentation for generics

Do you mean the builtin types such as list and set?
How does the following sound:

generics disallows usage of generic types that do not specify explicit type parameters. Moreover, built-in collections, such as list and dict, are disallowed as you should use their aliases from typing module (such as List and Dict).


What about an option for just rejecting variables with an inferred (i.e. not annotated) Any type?

Yes, I think it's a good idea. The option sounds very similar to untyped but untyped refers to functions whereas inferred would refer to variables. If we were to have both untyped and inferred, we should probably rename the two options to sound similar.


Another option that was discussed before was a subset of your proposed expr where Any types can only be used 'safely' in expressions.

This sounds like a good idea. Otherwise the expr option is not usable.
Do I understand you correctly: variables of type Any are allowed but you can only use them in the special cases you described (first argument to isinstance, second argument to cast, or as a function argument when expected type is Any or object)?

@ilinum
Copy link
Collaborator Author

ilinum commented Jul 20, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants