Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get Python variable annotations?

When defining a class/module with annotated fields, how can I get annotations as like in functions?

class Test:
  def __init__(self):
    self.x : int
t = Test()

Now I need 'int' from getattr(t,'x')

like image 784
Kamyar Avatar asked Jul 10 '19 09:07

Kamyar


3 Answers

With baseline Python, there is no option to do what you want without changing the definition of Test. The minimalist change would be to annotate the attribute at class level:

class Test:
    x: int
    def __init__(self):
        # define self.x or not, but it needn't be annotated again

This is actually perfectly fine; by default, annotations at class scope are assumed to refer to instance attributes, not class attributes (assigning to a value at class scope creates a class attribute, but annotating it does not); you have to explicitly use typing.ClassVar to indicate the annotated type is intended to be a class attribute only. PEP 526's section on class and instance variable annotations defines these behaviors; they're something you can rely on, not just an accident of implementation.

Once you've done this, typing.get_type_hints will return {'x': int} for both Test and t in your example case.

While that's enough on its own, I'll note that in many such cases nowadays, as long as you're annotating anyway, you can simplify your code with the dataclasses module, getting the annotations and basic functionality defined for you with minimal typing. Simple replacement code for your case would be:

import dataclasses

@dataclasses.dataclass
class Test:
    x: int

While your case doesn't showcase the full feature set (it's basically just replacing __init__ with the decorator), it's still doing more than meets the eye. In addition to defining __init__ for you (it expects to receive an x argument which is annotated to be an int), as well as a suitable __repr__ and __eq__, you can define defaults easily (just assign the default at point of annotation or for more complex or mutable cases, assign a dataclasses.field instead), and you can pass arguments to dataclass to make it produce sortable or immutable instances.

In your case, the main advantage is removing redundancy; x is annotated and referenced exactly once, rather than being annotated once at class level, then used (and optionally, annotated again) during initialization.

like image 103
ShadowRanger Avatar answered Oct 07 '22 03:10

ShadowRanger


I am not sure you can get the annotations of self.x easily.

Assuming your code:

class Test:
    def __init__(self):
        self.x: int = None

t = Test()

I tried looking for __annotations__ in Test and t (where I would expect it to be), without much luck.

However, what you could do is this workaround:

class Test:
    x: int
    def __init__(self):
        # annotation from here seems to be unreachable from `__annotations__`
        self.x: str

t = Test()

print(Test.__annotations__)
# {'x': <class 'int'>}
print(t.__annotations__)
# {'x': <class 'int'>}

EDIT

If you want to be able to inspect the type of self.x within mypy check answer from @ruohola.


EDIT 2

Note that mypy (at least v.0.560) does get confused by annotating x both from the class and from the __init__, i.e. it looks like the annotation of self.x is boldly ignored:

import sys

class Test:
    x: str = "0"
    def __init__(self):
        self.x: int = 1

t = Test()

print(Test.x, t.x)
# 0 1
print(Test.x is t.x)
# False

if "mypy" in sys.modules:
    reveal_type(t.x)
    # from mypyp: annotated_self.py:14: error: Revealed type is 'builtins.str'
    reveal_type(Test.x)
    # from mypy: annotated_self.py:15: error: Revealed type is 'builtins.str'

Test.x = 2
# from mypy: annotated_self.py:17: error: Incompatible types in assignment (expression has type "int", variable has type "str")

t.x = "3"
# no complaining from `mypy`
t.x = 4
# from mypy: annotated_self.py:19: error: Incompatible types in assignment (expression has type "int", variable has type "str")

print(Test.x, t.x)
# 2 4
like image 45
norok2 Avatar answered Oct 07 '22 02:10

norok2


If you're using mypy, you can use reveal_type() to check the type annotation of any expression. Note that this function is only usable when running mypy, and not at normal runtime. I also use typing.TYPE_CHECKING, to not get an error when running the file normally, since this special constant is only assumed to be True by 3rd party type checkers.

test.py:

from typing import Dict, Optional, TYPE_CHECKING


class Test:
    def __init__(self) -> None:
        self.x: Optional[Dict[str, int]]


test = Test()
if TYPE_CHECKING:
    reveal_type(test.x)
else:
    print("not running with mypy")

Example when running mypy on it:

$ mypy test.py
test.py:10: error: Revealed type is 'Union[builtins.dict[builtins.str, builtins.int], None]'

And when running it normally:

$ python3 test.py
not running with mypy
like image 27
ruohola Avatar answered Oct 07 '22 02:10

ruohola