Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I type hint a method with the type of the enclosing class?

I have the following code in Python 3:

class Position:      def __init__(self, x: int, y: int):         self.x = x         self.y = y      def __add__(self, other: Position) -> Position:         return Position(self.x + other.x, self.y + other.y) 

But my editor (PyCharm) says that the reference Position can not be resolved (in the __add__ method). How should I specify that I expect the return type to be of type Position?

Edit: I think this is actually a PyCharm issue. It actually uses the information in its warnings, and code completion.

But correct me if I'm wrong, and need to use some other syntax.

like image 573
Michael van Gerwen Avatar asked Nov 04 '15 22:11

Michael van Gerwen


People also ask

How do you write a hint in a class?

In a type hint, if we specify a type (class), then we mark the variable as containing an instance of that type. To specify that a variable instead contains a type, we need to use type[Cls] (or the old syntax typing. Type ). We need to add type hints for make_animal() .

What does type () do in Python?

Syntax of the Python type() function The type() function is used to get the type of an object. When a single argument is passed to the type() function, it returns the type of the object. Its value is the same as the object.

What is __ add __ in Python?

The __add__() method in Python specifies what happens when you call + on two objects. When you call obj1 + obj2, you are essentially calling obj1. __add__(obj2). For example, let's call + on two int objects: n1 = 10.

Are type hints Pythonic?

Type hints work best in modern Pythons. Annotations were introduced in Python 3.0, and it's possible to use type comments in Python 2.7. Still, improvements like variable annotations and postponed evaluation of type hints mean that you'll have a better experience doing type checks using Python 3.6 or even Python 3.7.


2 Answers

Specifying the type as string is fine, but always grates me a bit that we are basically circumventing the parser. So you better not misspell any one of these literal strings:

def __add__(self, other: 'Position') -> 'Position':     return Position(self.x + other.x, self.y + other.y) 

A slight variation is to use a bound typevar, at least then you have to write the string only once when declaring the typevar:

from typing import TypeVar  T = TypeVar('T', bound='Position')  class Position:      def __init__(self, x: int, y: int):         self.x = x         self.y = y      def __add__(self, other: T) -> T:         return Position(self.x + other.x, self.y + other.y) 
like image 30
vbraun Avatar answered Sep 27 '22 20:09

vbraun


TL;DR: As of today (2019), in Python 3.7+ you must turn this feature on using a "future" statement, from __future__ import annotations.

(The behaviour enabled by from __future__ import annotations might become the default in future versions of Python, and was going to be made the default in Python 3.10. However, the change in 3.10 was reverted at the last minute, and now may not happen at all.)

In Python 3.6 or below, you should use a string.


I guess you got this exception:

NameError: name 'Position' is not defined 

This is because Position must be defined before you can use it in an annotation, unless you are using Python with PEP 563 changes enabled.

Python 3.7+: from __future__ import annotations

Python 3.7 introduces PEP 563: postponed evaluation of annotations. A module that uses the future statement from __future__ import annotations will store annotations as strings automatically:

from __future__ import annotations  class Position:     def __add__(self, other: Position) -> Position:         ... 

This had been scheduled to become the default in Python 3.10, but this change has now been postponed. Since Python still is a dynamically typed language so no type-checking is done at runtime, typing annotations should have no performance impact, right? Wrong! Before Python 3.7, the typing module used to be one of the slowest python modules in core so for code that involves importing the typing module, you will see an up to 7 times increase in performance when you upgrade to 3.7.

Python <3.7: use a string

According to PEP 484, you should use a string instead of the class itself:

class Position:     ...     def __add__(self, other: 'Position') -> 'Position':        ... 

If you use the Django framework, this may be familiar, as Django models also use strings for forward references (foreign key definitions where the foreign model is self or is not declared yet). This should work with Pycharm and other tools.

Sources

The relevant parts of PEP 484 and PEP 563, to spare you the trip:

Forward references

When a type hint contains names that have not been defined yet, that definition may be expressed as a string literal, to be resolved later.

A situation where this occurs commonly is the definition of a container class, where the class being defined occurs in the signature of some of the methods. For example, the following code (the start of a simple binary tree implementation) does not work:

class Tree:     def __init__(self, left: Tree, right: Tree):         self.left = left         self.right = right 

To address this, we write:

class Tree:     def __init__(self, left: 'Tree', right: 'Tree'):         self.left = left         self.right = right 

The string literal should contain a valid Python expression (i.e., compile(lit, '', 'eval') should be a valid code object) and it should evaluate without errors once the module has been fully loaded. The local and global namespace in which it is evaluated should be the same namespaces in which default arguments to the same function would be evaluated.

and PEP 563:

Implementation

In Python 3.10, function and variable annotations will no longer be evaluated at definition time. Instead, a string form will be preserved in the respective __annotations__ dictionary. Static type checkers will see no difference in behavior, whereas tools using annotations at runtime will have to perform postponed evaluation.

...

Enabling the future behavior in Python 3.7

The functionality described above can be enabled starting from Python 3.7 using the following special import:

from __future__ import annotations 

Things that you may be tempted to do instead

A. Define a dummy Position

Before the class definition, place a dummy definition:

class Position(object):     pass   class Position(object):     ... 

This will get rid of the NameError and may even look OK:

>>> Position.__add__.__annotations__ {'other': __main__.Position, 'return': __main__.Position} 

But is it?

>>> for k, v in Position.__add__.__annotations__.items(): ...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                   return is Position: False other is Position: False 

B. Monkey-patch in order to add the annotations:

You may want to try some Python metaprogramming magic and write a decorator to monkey-patch the class definition in order to add annotations:

class Position:     ...     def __add__(self, other):         return self.__class__(self.x + other.x, self.y + other.y) 

The decorator should be responsible for the equivalent of this:

Position.__add__.__annotations__['return'] = Position Position.__add__.__annotations__['other'] = Position 

At least it seems right:

>>> for k, v in Position.__add__.__annotations__.items(): ...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                   return is Position: True other is Position: True 

Probably too much trouble.

like image 122
Paulo Scardine Avatar answered Sep 27 '22 21:09

Paulo Scardine