66

I've just begun playing around with Python's Data Classes, and I would like confirm that I am declaring Class Variables in the proper way.

Using regular python classes

class Employee:

    raise_amount = .05

    def __init__(self, fname, lname, pay):
        self.fname = fname
        self.lname = lname
        self.pay = pay

Using python Data Class

@dataclass
class Employee:
    fname: str
    lname: str
    pay: int
    raise_amount = .05

The class variable I am referring to is raise_amount. Is this a properly declared class variable using Data Classes? Or is there a better way of doing so?

I have tested the data class implementation already and it provides the expected functionality, but I am mainly wondering if my implementation is following best practices.

Kurt Kline
  • 1,724
  • 1
  • 10
  • 23

2 Answers2

95

To create a class variable, annotate the field as a typing.ClassVar or not at all.

from typing import ClassVar
from dataclasses import dataclass

@dataclass
class Foo:
    ivar: float = 0.5
    cvar: ClassVar[float] = 0.5
    nvar = 0.5

foo = Foo()
Foo.ivar, Foo.cvar, Foo.nvar = 1, 1, 1
print(Foo().ivar, Foo().cvar, Foo().nvar)   # 0.5 1 1
print(foo.ivar, foo.cvar, foo.nvar)         # 0.5 1 1
print(Foo(), Foo(12))                       # Foo(ivar=0.5) Foo(ivar=12)

There is a subtle difference in that the unannotated field is completely ignored by @dataclass, whereas the ClassVar field is stored but not converted to an attribute.


dataclasses — Data Classes

The member variables [...] are defined using PEP 526 type annotations.

Class variables

One of two places where dataclass() actually inspects the type of a field is to determine if a field is a class variable as defined in PEP 526. It does this by checking if the type of the field is typing.ClassVar. If a field is a ClassVar, it is excluded from consideration as a field and is ignored by the dataclass mechanisms. Such ClassVar pseudo-fields are not returned by the module-level fields() function.

MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119
  • My original setup `raise_amount = .05` appears to have the same functionality as `raise_amount: ClassVar[float] = .05`. If the data type is missing, do you know if it automatically assumes a variable is a class variable? I do like how your implementation is more explicit though. – Kurt Kline May 21 '20 at 16:09
  • 6
    If a variable is not annotated, it is ignored by ``dataclass`` completely. That effectively makes it a class variable as well. – MisterMiyagi May 21 '20 at 16:11
  • Got it, makes sense. Thank you. I think it's better to keep things obvious, so your method is probably the way to go. Much appreciated. – Kurt Kline May 21 '20 at 16:12
  • 2
    Worth noting, `dataclasses.fields(Foo)` won't show `cvar`, but it will be in `Foo.__dataclass_fields__` – juanpa.arrivillaga Jul 19 '22 at 20:14
7

You should annotate class variable with typing.ClassVar for mypy to capture errors when a class variable is assigned a value via an instance of the class.

Leaving out annotation completely won't help mypy.

from dataclasses import dataclass
from typing import ClassVar


@dataclass
class Class:
    name: ClassVar[str]
    desc = "default description"


instance = Class()
instance.desc = "instance description"
instance.name = "instance name"

Class.name = "class name"
Class.desc = "class description"

Mypy output;

error: Cannot assign to class variable "name" via instance
Nizam Mohamed
  • 8,751
  • 24
  • 32
  • ...and it is a good thing that you can not assign to class variables via instance! Standard Python should have that feature. – Robert Siemer Dec 03 '22 at 10:57
  • @RobertSiemer Actually there is a pythonic pattern where class vars act like default values. If you don’t want the default value you can overwrite it with an instance var. – cocoafan Aug 19 '23 at 11:50