Skip to content
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

Implement Const type to indicate non-rebindable variables/attributes #1214

Closed
hashstat opened this issue Feb 11, 2016 · 3 comments · Fixed by #5522
Closed

Implement Const type to indicate non-rebindable variables/attributes #1214

hashstat opened this issue Feb 11, 2016 · 3 comments · Fixed by #5522

Comments

@hashstat
Copy link

As a Python developer, I would like a Const type so that I can indicate that a variable or attribute may not be rebound to another value. This would be especially useful for module and class constants, but could also be helpful for instance attributes that really shouldn't be modified. Not only does it help prevent accidentally overwriting constants, it would hopefully reduce the need to use @property on non-critical attributes. Here is a contrived example:

GRUNT = 0   # type: Const
MANAGER = 1   # type: Const


class Employee:
    kind = GRUNT   # type: Const

    def __init__(self, idnum: int, name: str):
        self.id = idnum   # type: Const
        self.name = name


class Manager(Employee):
    kind = MANAGER   # type: Const

kind = MANAGER # type: Const reqiures the Const type to rebind kind without an error just as a const variable in C requires a cast during reassignment to quiet compiler warnings/errors.

I'm not sure if it makes sense to allow Const to take parameters as the type can be inferred in most cases. One might want other variables, such as those passed into functions, to be const, in which case it makes sense to allow Const to take parameters. Here is an example:

def frobnicate(kind: Const[int]) -> None:
    kind = 5   # Error
    kind = 6   # type: Const  # Okay
@JukkaL
Copy link
Collaborator

JukkaL commented Feb 19, 2016

Early in the history of mypy I was actually planning to have something like this, but I guess I later forgot about it. This would likely require a PEP 484 (and potentially typing) change.

@davidroeca
Copy link

Const in Python could have similar guarantees to something like JavaScript ES6's const, to prevent reassignment https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const

A clever programmer would be able to get around this through methods that mutate, or by a shared reference, but the guard rails are still nice.

Alternatively, it could only be allowed for immutable non-container primitives such as int or str, or maybe even extended to tuples/namedtuples of immutable primitives as well

@JukkaL
Copy link
Collaborator

JukkaL commented May 18, 2018

We are now considering something like this, perhaps spelled as Final. An interesting use case would be with literal types (another potential new feature) -- it would allow mypy to infer things like if X: never being taken if there is X: Final = False.

@JukkaL JukkaL added priority-1-normal false-positive mypy gave an error on correct code and removed priority-2-low labels May 18, 2018
ilevkivskyi added a commit 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants