15.3. OOP ClassVar

  • Class Variables

  • Instance Variables

  • Type Annotations

15.3.1. Class Variables

  • Fields defined on a class

  • Must have default values

  • Share state

  • Also known as 'static attributes'

Class variables are defined on a class:

>>> class User:
...     pass
>>>
>>>
>>> User.firstname = 'Mark'
>>> User.lastname = 'Watney'

Class variables are defined in a class:

>>> class User:
...     firstname = 'Mark'
...     lastname = 'Watney'

In order to show all the variables use vars() on the class:

>>> vars(User)  
mappingproxy({'__module__': '__main__',
              'firstname': 'Mark',
              'lastname': 'Watney',
              '__dict__': <attribute '__dict__' of 'User' objects>,
              '__weakref__': <attribute '__weakref__' of 'User' objects>,
              '__doc__': None})

15.3.2. Instance Variables

  • Fields defined on an instance

  • Do not share state (unless mutable argument in method signature)

  • By convention initialized in __init__()

  • Also known as 'dynamic attributes'

Instance variables are defined on an instance:

>>> class User:
...     pass
>>>
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'

Instance variables are defined in init:

>>> class User:
...     def __init__(self):
...         self.firstname = 'Mark'
...         self.lastname = 'Watney'

Instance variables with variable values:

>>> class User:
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname

In order to show all the variables use vars() on an instance:

>>> mark = User('Mark', 'Watney')
>>>
>>> vars(mark)
{'firstname': 'Mark', 'lastname': 'Watney'}

15.3.3. Class and Instance Variables

Class and instance variables defined in code:

>>> class User:
...     pass
>>>
>>>
>>> User.firstname = 'Mark'
>>> User.lastname = 'Watney'
>>>
>>> mark = User()
>>> mark.firstname = 'Melissa'
>>> mark.lastname = 'Lewis'

Class and instance variables defined in class:

>>> class User:
...     firstname = 'Mark'
...     lastname = 'Watney'
...
...     def __init__(self):
...         self.firstname = 'Mark'
...         self.lastname = 'Watney'

Note, the last example makes not meaningful sense. Instance variables will shadow class variables.

>>> vars(User)  
mappingproxy({'__module__': '__main__',
              'firstname': 'Mark',
              'lastname': 'Watney',
              '__init__': <function User.__init__ at 0x...>,
              '__dict__': <attribute '__dict__' of 'User' objects>,
              '__weakref__': <attribute '__weakref__' of 'User' objects>,
              '__doc__': None})
>>> mark = User()
>>> vars(mark)
{'firstname': 'Mark', 'lastname': 'Watney'}

15.3.4. Annotations

Type annotations are not variable definition:

>>> x: int
>>>
>>> print(x)
Traceback (most recent call last):
NameError: name 'x' is not defined

Type annotations will only tell, that if there will be an identifier with name x then it should be an int:

>>> x: int
>>> x = 1
>>>
>>> print(x)
1

Typically it is written in shorter form:

>>> x: int = 1
>>>
>>> print(x)
1

These are not attributes at all (sic!). These are type annotations only, and they do not exist before initialization in a code:

>>> class User:
...     firstname: str
...     lastname: str
>>> vars(User)  
mappingproxy({'__module__': '__main__',
              '__annotations__': {'firstname': <class 'str'>, 'lastname': <class 'str'>},
              '__dict__': <attribute '__dict__' of 'User' objects>,
              '__weakref__': <attribute '__weakref__' of 'User' objects>,
              '__doc__': None})

Class variables with type annotations:

>>> class User:
...     firstname: str = 'Mark'
...     lastname: str = 'Watney'
>>> vars(User)  
mappingproxy({'__module__': '__main__',
              '__annotations__': {'firstname': <class 'str'>, 'lastname': <class 'str'>},
              'firstname': 'Mark',
              'lastname': 'Watney',
              '__dict__': <attribute '__dict__' of 'User' objects>,
              '__weakref__': <attribute '__weakref__' of 'User' objects>,
              '__doc__': None})

Class variables with proper type annotations:

>>> from typing import ClassVar
>>>
>>>
>>> class User:
...     firstname: ClassVar[str] = 'Mark'
...     lastname: ClassVar[str] = 'Watney'

Instance variables with type annotations:

>>> class User:
...     firstname: str
...     lastname: str
...
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname

15.3.5. Dataclass Fields

  • Dataclass uses class variables notation to create instance fields

  • Dataclass do not validate type annotations, unless ClassVar

>>> from dataclasses import dataclass
>>> from typing import ClassVar

Instance variables:

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
>>> vars(User)  
mappingproxy({'__module__': '__main__',
              '__annotations__': {'firstname': <class 'str'>, 'lastname': <class 'str'>},
              '__dict__': <attribute '__dict__' of 'User' objects>,
              '__weakref__': <attribute '__weakref__' of 'User' objects>,
              '__doc__': 'User(firstname: str, lastname: str)',
              '__dataclass_params__': _DataclassParams(init=True,repr=True,eq=True,order=False,unsafe_hash=False,frozen=False,match_args=True,kw_only=False,slots=False,weakref_slot=False),
              '__dataclass_fields__': {'firstname': Field(name='firstname',type=<class 'str'>,default=<dataclasses._MISSING_TYPE object at 0x...>,default_factory=<dataclasses._MISSING_TYPE object at 0x...>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD),
               'lastname': Field(name='lastname',type=<class 'str'>,default=<dataclasses._MISSING_TYPE object at 0x...>,default_factory=<dataclasses._MISSING_TYPE object at 0x...>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)},
              '__init__': <function User.__init__ at 0x...>,
              '__repr__': <function User.__repr__ at 0x...>,
              '__eq__': <function User.__eq__ at 0x...>,
              '__hash__': None,
              '__match_args__': ('firstname', 'lastname')})

Instance variables with default values:

>>> @dataclass
... class User:
...     firstname: str = 'Mark'
...     lastname: str = 'Watney'
>>> vars(User)  
mappingproxy({'__module__': '__main__',
              '__annotations__': {'firstname': <class 'str'>, 'lastname': <class 'str'>},
              'firstname': 'Mark',
              'lastname': 'Watney',
              '__dict__': <attribute '__dict__' of 'User' objects>,
              '__weakref__': <attribute '__weakref__' of 'User' objects>,
              '__doc__': "User(firstname: str = 'Mark', lastname: str = 'Watney')",
              '__dataclass_params__': _DataclassParams(init=True,repr=True,eq=True,order=False,unsafe_hash=False,frozen=False,match_args=True,kw_only=False,slots=False,weakref_slot=False),
              '__dataclass_fields__': {'firstname': Field(name='firstname',type=<class 'str'>,default='Mark',default_factory=<dataclasses._MISSING_TYPE object at 0x...>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD),
               'lastname': Field(name='lastname',type=<class 'str'>,default='Watney',default_factory=<dataclasses._MISSING_TYPE object at 0x...>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)},
              '__init__': <function User.__init__ at 0x...>,
              '__repr__': <function User.__repr__ at 0x...>,
              '__eq__': <function User.__eq__ at 0x...>,
              '__hash__': None,
              '__match_args__': ('firstname', 'lastname')})
>>> mark = User()
>>> vars(mark)
{'firstname': 'Mark', 'lastname': 'Watney'}

Class variables must have default values:

>>> @dataclass
... class User:
...     firstname: ClassVar[str] = 'Mark'
...     lastname: ClassVar[str] = 'Watney'

15.3.6. Init Variables

>>> from dataclasses import InitVar
>>> @dataclass
... class User:
...     firstname: InitVar[str] = 'Mark'
...     lastname: InitVar[str] = 'Watney'

15.3.7. Class vs. Instance Variables

Lets define a class with class variable:

>>> class User:
...     agency = 'NASA'

Lets create three instances of User class:

>>> mark = User()
>>> melissa = User()
>>> rick = User()

We will print agency field:

>>> print(mark.agency)
NASA
>>>
>>> print(melissa.agency)
NASA
>>>
>>> print(rick.agency)
NASA
>>>
>>> print(User.agency)
NASA

Lets change field on a class and print agency field:

>>> User.agency = 'ESA'
>>>
>>>
>>> print(mark.agency)
ESA
>>>
>>> print(melissa.agency)
ESA
>>>
>>> print(rick.agency)
ESA
>>>
>>> print(User.agency)
ESA

Lets change field on an instance and print agency field:

>>> mark.agency = 'POLSA'
>>>
>>>
>>> print(mark.agency)
POLSA
>>>
>>> print(melissa.agency)
ESA
>>>
>>> print(rick.agency)
ESA
>>>
>>> print(User.agency)
ESA

Note, that the class which defined instance variable shadowed the class variable.

Lets change field on a class and print agency field:

>>> User.agency = 'NASA'
>>>
>>>
>>> print(mark.agency)
POLSA
>>>
>>> print(melissa.agency)
NASA
>>>
>>> print(rick.agency)
NASA
>>>
>>> print(User.agency)
NASA

Lets delete field from an instance and print agency field:

>>> del mark.agency
>>>
>>>
>>> print(mark.agency)
NASA
>>>
>>> print(melissa.agency)
NASA
>>>
>>> print(rick.agency)
NASA
>>>
>>> print(User.agency)
NASA

15.3.8. Mechanism

  • vars(obj) is will return obj.__dict__

>>> class User:
...     firstname = 'Mark'
...     lastname = 'Watney'
...
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
>>>
>>>
>>> melissa = User('Melissa', 'Lewis')
>>>
>>> vars(melissa)
{'firstname': 'Melissa', 'lastname': 'Lewis'}
>>>
>>> vars(User)  
mappingproxy({'__module__': '__main__',
              'firstname': 'Mark',
              'lastname': 'Watney',
              '__init__': <function User.__init__ at 0x...>,
              '__dict__': <attribute '__dict__' of 'User' objects>,
              '__weakref__': <attribute '__weakref__' of 'User' objects>,
              '__doc__': None})

15.3.9. Use Case - 0x01

>>> from typing import ClassVar
>>>
>>>
>>> class User:
...     firstname: str
...     lastname: str
...     age: int
...     AGE_MIN: ClassVar[int] = 30
...     AGE_MAX: ClassVar[int] = 50

15.3.10. Use Case - 0x02

>>> from typing import ClassVar
>>>
>>>
>>> class User:
...     firstname: str
...     lastname: str
...     age: int
...     AGE_MIN: ClassVar[int] = 30
...     AGE_MAX: ClassVar[int] = 50
...
...     def __init__(self, firstname, lastname, age):
...         self.firstname = firstname
...         self.lastname = lastname
...         self.age = age
...
...         if not self.AGE_MIN <= self.age < self.AGE_MAX:
...             raise ValueError('age is invalid')

15.3.11. Use Case - 0x03

>>> from dataclasses import dataclass
>>> from typing import ClassVar
>>>
>>>
>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     age: int
...     AGE_MIN: ClassVar[int] = 30
...     AGE_MAX: ClassVar[int] = 50
...
...     def __post_init__(self):
...         if not self.AGE_MIN <= self.age < self.AGE_MAX:
...             raise ValueError('age is invalid')

15.3.12. Assignments

Code 15.13. Solution
"""
* Assignment: OOP AttributeClassVar Class
* Complexity: easy
* Lines of code: 2 lines
* Time: 2 min

English:
    1. Define class User with class variables:
        a. firstname: str (default 'Mark')
        b. lastname: str (default 'Watney')
    2. Do not use `dataclass`
    3. Do not use `typing`
    4. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę User z polami klasowymi:
        a. firstname: str (domyślnie 'Mark')
        b. lastname: str (domyślnie 'Watney')
    2. Nie używaj `dataclass`
    3. Nie używaj `typing`
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass

    >>> assert isclass(User)
    >>> assert hasattr(User, '__annotations__')

    >>> fields = User.__dict__
    >>> assert 'firstname' in fields
    >>> assert 'lastname' in fields
"""


# Define class User with class variables:
# - firstname: str (default 'Mark')
# - lastname: str (default 'Watney')
# Do not use `dataclass`
# Do not use `typing`
# type: type[User]
class User:
    pass


Code 15.14. Solution
# TODO: Przenieść zadanie do Advanced/typing, w Intermediate typing nie są omawiane
"""
* Assignment: OOP AttributeClassVar TypeAnnotations
* Complexity: easy
* Lines of code: 2 lines
* Time: 2 min

English:
    1. Define class User with class variables:
        a. firstname: str (default 'Mark')
        b. lastname: str (default 'Watney')
    2. Do not use `dataclass`
    3. Use `typing`
    4. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę User z polami klasowymi:
        a. firstname: str (domyślnie 'Mark')
        b. lastname: str (domyślnie 'Watney')
    2. Nie używaj `dataclass`
    3. Użyj `typing`
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass

    >>> assert isclass(User)
    >>> assert hasattr(User, '__annotations__')

    >>> fields = User.__dict__
    >>> assert 'firstname' in fields
    >>> assert 'lastname' in fields
"""
from typing import ClassVar


# Define class User with class variables:
# - firstname: str (default 'Mark')
# - lastname: str (default 'Watney')
# Do not use `dataclass`
# Use `typing`
# type: type[User]
class User:
    pass


Code 15.15. Solution
# TODO: Przenieść zadanie do Advanced/dataclass, w Intermediate dataclass nie są omawiane
"""
* Assignment: OOP AttributeClassVar Dataclass
* Complexity: easy
* Lines of code: 2 lines
* Time: 2 min

English:
    1. Define class User with class variables:
        a. firstname: str (no default value)
        b. lastname: str (no default value)
    2. Use `dataclass`
    3. Use `typing`
    4. Run doctests - all must succeed

Polish:
    1. Zdefiniuj klasę User z polami klasowymi:
        a. firstname: str (bez domyślnej wartości)
        b. lastname: str (bez domyślnej wartości)
    2. Użyj `dataclass`
    3. Użyj `typing`
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass
    >>> from dataclasses import _FIELD_CLASSVAR

    >>> assert isclass(User)
    >>> assert hasattr(User, '__annotations__')
    >>> assert hasattr(User, '__dataclass_fields__')

    >>> fields = User.__dataclass_fields__
    >>> assert 'firstname' in fields
    >>> assert 'lastname' in fields
    >>> assert fields['firstname']._field_type is _FIELD_CLASSVAR
    >>> assert fields['lastname']._field_type is _FIELD_CLASSVAR
"""
from dataclasses import dataclass
from typing import ClassVar


# Define class User with class variables:
# - firstname: str (no default value)
# - lastname: str (no default value)
# Use `dataclass` and `typing`
# type: type[User]
@dataclass
class User:
    pass