Skip to content
New issue

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

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

Already on GitHub? # to your account

Proposal to add const-ness type hints #242

Closed
sivachandra opened this issue Jul 11, 2016 · 2 comments · Fixed by python/mypy#5522
Closed

Proposal to add const-ness type hints #242

sivachandra opened this issue Jul 11, 2016 · 2 comments · Fixed by python/mypy#5522

Comments

@sivachandra
Copy link

Motivation - Python does not have a concept or notion of object const-ness. The const-ness is normally conveyed via doc strings which explain the usage contracts of the frameworks involved. However, this does not provide the necessary formalism required for static analysis tools (like pytype) to flag const-ness contract violations. Hence, we propose to add const-ness type hints to PEP-484 which can be used by static analysis tools to flag const-ness contract violations.

The Proposal

We propose adding two constructs:

The @constmethod decorator

This decorator can only be used to decorate instance methods. It describes methods which do not modify the instances on which they are invoked. That is, a method decorated with this decorator a) should not modify the fields on the object, b) should not call other methods which modify the fields of the object, c) and it should not pass non-primitive fields of the object to other callables which modify them.

Normally, this decorator would be used on getters. For example:

from typing import constmethod


class MyClass(object):
    ...
    @constmethod
    def get_something(self) -> int: ...

The protocol type-hint Const

This type-hint is used to describe function arguments and return values. When used to describe a function argument, it indicates that the function argument will not be modified by the function. It should only be used for non-primitive types. Example:

from typing import Const


def my_function(alist: Const[List[SomeClass]]) -> int: ...

The above indicates that the list argument alist will not be modified by my_function. Note that it does not indicate that the elements of the list are also not modified. If it is desired to indicate that even the elements are not modified, then one should use:

def my_function(alist: Const[List[Const[SomeClass]]]) -> int: ...

When Const is used as a type-hint on return values, it indicates that the value returned should not be modified by the caller. Example:

def my_function() -> Const[List[SomeClass]]: ...

This indicates that the returned list object should not be modified by the caller. As before, it does not indicate that the elements of the returned list should also not be modified. If even element const-ness is desired, then one should use:

def my_function() -> Const[List[Const[SomeClass]]]: ...

@gvanrossum
Copy link
Member

I shudder at the thought of having to annotate any significant piece of code with this.

I recommend that you try to implement a proof-of-concept prototype as a modification of mypy first and then see what it would take to productionize this, before we formally add this to PEP 484.

ilevkivskyi added a commit to python/mypy that referenced this issue Sep 11, 2018
Fixes #1214
Fixes python/typing#286
Fixes python/typing#242 (partially, other part is out of scope)


This is a working implementation of final access qualifier briefly discussed at PyCon typing meeting. Final names/attributes can be used to have more static guarantees about semantics of some code and can be used by other tools like mypyc for optimizations.

We can play with this implementation before starting to write an actual PEP.

The basic idea is simple: once declared as final, a name/attribute can't be re-assigned, overridden, or redefined in any other way. For example:
```python
from typing import Final

NO: Final = 0
YES: Final = 255

class BaseEngine:
    RATE: Final[float] = 3000

YES = 1  # Error!

class Engine(BaseEngine):
    RATE = 9000  # Also an error!
```
For more use cases, examples, and specification, see the docs patch.

Here are some comments on decisions made:
* __What can be final?__ It is hard to say what semantic nodes are important, I started from just module and class constants, but quickly realized it is hard to draw the line without missing some use cases (in particular for mypyc). So I went ahead and implemented all of them, everything can be final: module constants, class-level and instance-level attributes, method, and also classes.
* __Two names or one name?__ I currently use two names `Final` for assignments and `@final` for decorators. My PEP8-formatted mind just can't accept `@Final` :-)
* __Should re-exported names keep they const-ness?__ I think yes, this is a very common pattern, so it looks like this is a sane default.
* __What to do with instance-level vs class-level attributes?__ The point here is that mypy has a common namespace for class attributes. I didn't want to complicate things (including the mental model), so I just decided that one can't have, e.g., a name that is constant on class but assignable on instances, etc. Such use cases are relatively rare, and we can implement this later if there will be high demand for this.

...deferred features:
* I didn't implement any constant propagation in mypy _yet_. This can be done later on per use-case 
  basis. For example:
  ```python
  fields: Final = [('x', int), ('y', int)]
  NT = NamedTuple('NT', fields)
  ```
* __Should final classes be like sealed in Scala?__ I think probably no. On one hand it could be be a nice feature, on other hand it complicates the mental model and is less useful for things like mypyc.
* I don't allow `Final` in function argument types. One argument is simplicity, another is I didn't see many bugs related to shadowing an argument in function bodies, finally people might have quite different expectations for this. If people will ask, this would be easy to implement.

...and implementation internals:
* There are two additional safety nets that I don't mention in the docs: (a) there can be no `TypeVar`s in the type of class-level constant, (b) instance-level constant can't be accessed on the class object.
* I generate errors for re-definitions in all subclasses, not only in immediate children. I think this is what most people would want: turning something into a constant will flag most re-assignment points.
* We store the `final_value` for constants initialized with a simple literal, but we never use it. This exists only for tools like mypyc that may use it for optimizations.

cc @ambv @rchen152 @vlasovskikh
@vnmabus
Copy link

vnmabus commented Sep 2, 2022

I also think that the protocol type-hint Const would be very useful. In particular, I think NumPy arrays are currently invariant with respect to their dtype, and I would want them to be contravariant in a function that don't modify them (similar to the proposal in numpy/numpy#16955, but without having to create and maintain a parallel const type).

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

Successfully merging a pull request may close this issue.

3 participants