Type annotating class variable: in init or body?

2024/10/8 8:33:36

Let's consider the two following syntax variations:

class Foo:x: intdef __init__(self, an_int: int):self.x = an_int

And

class Foo:def __init__(self, an_int: int):self.x = an_int

Apparently the following code raises a mypy error in both cases (which is expected):

obj = Foo(3)
obj.x.title()  # this is a str operation

But I really want to enforce the contract: I want to make it clear that x is an instance variable of every Foo object. So which syntax should be preferred, and why?

Answer

This is ultimately a matter of personal preference. To use the example in the other answer, doing both:

class Foo:x: Union[int, str]def __init__(self, an_int: int) -> None:self.x = an_int

...and doing:

class Foo:def __init__(self, an_int: int) -> None:self.x: Union[int, str] = an_int

...will be treated in the exact same way by type checkers.

The main advantage of doing the former is that it makes the types of your attributes more obvious in the cases where your constructor is complex to the point where it's difficult to trace what type inference is being performed.

This style is also consistent with how you declare and use things like dataclasses:

from dataclasses import dataclass@dataclass
class Foo:x: inty: Union[int, str]z: str# You get an `__init__` for free. Mypy will check to make sure the types match.
# So this type checks:
a = Foo(1, "b", "c")# ...but this doesn't:
b = Foo("bad", 3.14, 0)

This isn't really a pro or a con, just more of an observation that the standard library has, in some specific cases, embraced the former style.

The main disadvantage is that this style is somewhat verbose: you're forced into repeating the variable name two times (three, if you include the __init__ parameter), and often forced into repeating the type hint twice (once in your variable annotation and once in in the __init__ signature).

It also opens up a possible correctness issue in your code: mypy will never actually check to make sure you've assigned anything to your attribute! For example, the following code will happily type check despite that it crashes at runtime:

class Foo:x: intdef __init__(self, x: int) -> None:# Whoops, I forgot to do 'self.x = x'passf = Foo(1)# Type checks, but crashes at runtime!
print(f.x)

The latter style dodges these issues: if you forget to assign an attribute, mypy will complain that it doesn't exist when you try using it later.

The other main advantage of the latter style is that you can also get away with not adding an explicit type hint a lot of the time, especially if you're just assigning a parameter directly to a field. The type checker will infer the exact same type in those cases.


So given these factors, my personal preference is to:

  1. Use dataclasses (and by proxy, the former style) if I just want a simple, record-like object with an automatically generated __init__.
  2. Use the latter style if I either feel dataclasses are overkill or need to write a custom __init__, to decrease both verbosity and the odds of running into the "forgot-to-assign-an-attribute" bug.
  3. Switch back to the former style if I have a sufficiently large and complex __init__ that's somewhat difficult to read. (Or better yet, just refactor my code so I can keep the __init__ simple!)

You may end up weighing these factors differently and come up with a different set of tradeoffs, of course.


One final tangent -- when you do:

class Foo:x: int

...you are not actually annotating a class variable. At this point, x has no value, so doesn't actually exist as a variable.

The only thing you're creating is an annotation, which is just pure metadata and distinct from the variable itself.

But if you do:

class Foo:x: int = 3

...then you are creating both a class variable and an annotation. Somewhat confusingly, while you may be creating a class variable/attribute (as opposed to an instance variable/attribute), mypy and other type checker will continue assuming that type annotation is meant to annotate specifically an instance attribute.

This inconsistency usually doesn't matter in practice, especially if you follow the general best practice of avoiding mutable default values for anything. But this may cause some surprises if you're trying to do something fancy.

If you want mypy/other type checkers to understand your annotation is a class variable annotation, you need to use the ClassVar type:

# Import this from 'typing_extensions' if you're using Python 3.7 or earlier
from typing import ClassVarclass Foo:x: ClassVar[int] = 3
https://en.xdnf.cn/q/70141.html

Related Q&A

decoding shift-jis: illegal multibyte sequence

Im trying to decode a shift-jis encoded string, like this:string.decode(shift-jis).encode(utf-8)to be able to view it in my program.When I come across 2 shift-jis characters, in hex "0x87 0x54&quo…

Add columns in pandas dataframe dynamically

I have following code to load dataframe import pandas as pdufo = pd.read_csv(csv_path) print(ufo.loc[[0,1,2] , :])which gives following output, see the structure of the csvCity Colors Reported Shape Re…

How do you add input from user into list in Python [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.Want to improve this question? Add details and clarify the problem by editing this post.Closed 9 years ago.Improve…

How to suppress matplotlib inline for a single cell in Jupyter Notebooks/Lab?

I was looking at matplotlib python inline on/off and this kind of solves the problem but when I do plt.ion() all of the Figures pop up (100s of figures). I want to keep them suppressed in a single cel…

Using django-filer, can I chose the folder that images go into, from Unsorted Uploads

Im using django-filer for the first time, and it looks great, and work pretty well.But all my images are being uploaded to the Unsorted Uploads folder, and I cant figure out a way to put them in a spec…

how to use distutils to create executable .zip file?

Python 2.6 and beyond has the ability to directly execute a .zip file if the zip file contains a __main__.py file at the top of the zip archive. Im wanting to leverage this feature to provide preview r…

Pass nested dictionary location as parameter in Python

If I have a nested dictionary I can get a key by indexing like so: >>> d = {a:{b:c}} >>> d[a][b] cAm I able to pass that indexing as a function parameter? def get_nested_value(d, pat…

Correct way to deprecate parameter alias in click

I want to deprecate a parameter alias in click (say, switch from underscores to dashes). For a while, I want both formulations to be valid, but throw a FutureWarning when the parameter is invoked with …

How to rename a file with non-ASCII character encoding to ASCII

I have the file name, "abc枚.xlsx", containing some kind of non-ASCII character encoding and Id like to remove all non-ASCII characters to rename it to "abc.xlsx".Here is what Ive t…

draw random element in numpy

I have an array of element probabilities, lets say [0.1, 0.2, 0.5, 0.2]. The array sums up to 1.0.Using plain Python or numpy, I want to draw elements proportional to their probability: the first eleme…