9.4. Accessor Descriptor
Add managed attributes to objects
Outsource functionality into specialized classes
Descriptors:
classmethod
,staticmethod
,property
, functions in general__del__(self)
is reserved when object is being deleted by garbage collector (destructor)__set_name()
After class creation, Python default metaclass will call it with cls and classname
Descriptors are a powerful feature in Python that allow you to customize the behavior of attribute access on a class. They are used to define how an attribute is accessed, set, or deleted on an instance of a class.
In Python, every attribute access on an object is handled by a descriptor. A descriptor is an object that defines one or more of the following methods:
__set_name__(self, owner, name)
- used to set the name of the attribute,__get__(self, instance, owner)
- used to get the value of the attribute,__set__(self, instance, value)
- used to set the value of the attribute,__delete__(self, instance)
- used to delete the attribute.
Descriptors can be used to implement a variety of features, such as lazy evaluation of attributes, type checking, and data validation. They are commonly used in frameworks like Django and Flask to provide database access and validation of user input.
>>> class MyDescriptor:
... def __set_name__(self, owner, name):
... print('SetName')
...
... def __set__(self, instance, value):
... print('Setter')
...
... def __get__(self, instance, owner):
... print('Getter')
...
... def __delete__(self, instance):
... print('Deleter')
>>>
>>>
>>> class User:
... name = MyDescriptor()
SetName
>>>
>>>
>>> mark = User()
>>>
>>> mark.name = 'Mark'
Setter
>>>
>>> mark.name
Getter
>>>
>>> del mark.name
Deleter
9.4.1. Protocol
__set_name__(self, owner, name) -> None
__get__(self, instance, owner) -> Any
__set__(self, instance, value) -> None
__delete__(self, instance) -> None
If any of those methods are defined for an object, it is said to be a descriptor.
—Raymond Hettinger
>>> from typing import Any
>>>
>>> class Descriptor:
... def __set_name__(self, owner: type, name: str) -> None: ...
... def __get__(self, instance: object, owner: type) -> Any: ...
... def __set__(self, instance: object, value: Any) -> None: ...
... def __delete__(self, instance: object) -> None: ...
9.4.2. Problem
If one field need to have certain behavior -> Property
If all fields need to have certain behavior -> Reflection
If selected fields need to have certain behavior -> Descriptor
>>> class Point:
... x: int = property()
... y: int = property()
... z: int
...
... @x.getter
... def x(self):
... return self._x
...
... @x.deleter
... def x(self):
... self._x = None
...
... @x.setter
... def x(self, value):
... if value < 0:
... raise ValueError('Value must be greater than 0')
... self._x = value
...
... @y.getter
... def y(self):
... return self._y
...
... @y.deleter
... def y(self):
... self._y = None
...
... @y.setter
... def y(self, value):
... if value < 0:
... raise ValueError('Value must be greater than 0')
... self._y = value
9.4.3. Solution
>>> class PositiveInteger:
... def __set_name__(self, owner, name):
... self.name = name
...
... def __get__(self, instance, owner):
... return instance.__dict__[self.name]
...
... def __set__(self, instance, value):
... if value < 0:
... raise ValueError(f'Value must be greater than 0')
... instance.__dict__[self.name] = value
...
... def __delete__(self, instance):
... instance.__dict__[self.name] = None
>>>
>>>
>>> class Point:
... x: int = PositiveInteger()
... y: int = PositiveInteger()
... z: int
9.4.4. Inheritance
>>> class Validator:
... def __set_name__(self, owner, name):
... self.name = name
...
... def __get__(self, instance, owner):
... return instance.__dict__[self.attrname]
...
... def __delete__(self, instance):
... instance.__dict__[self.attrname] = None
>>>
>>>
>>> class PositiveInteger(Validator):
... def __set__(self, instance, value):
... if value < 0:
... raise ValueError(f'Value must be greater than 0')
... instance.__dict__[self.attrname] = value
>>>
>>>
>>> class NegativeInteger(Validator):
... def __set__(self, instance, value):
... if value > 0:
... raise ValueError(f'Value must be less than 0')
... instance.__dict__[self.attrname] = value
>>>
>>>
>>> class Point:
... x: int = PositiveInteger()
... y: int = PositiveInteger()
... z: int = NegativeInteger()
9.4.5. Builtin Descriptors
Function are Descriptors
Classes are Descriptors
Methods are Descriptors
Function are Descriptors:
>>> def hello():
... pass
>>>
>>>
>>> type(hello)
<class 'function'>
>>>
>>>
>>> hasattr(hello, '__set_name__')
False
>>>
>>> hasattr(hello, '__get__')
True
>>>
>>> hasattr(hello, '__set__')
False
>>>
>>> hasattr(hello, '__delete__')
False
Methods are Descriptors:
>>> class User:
... def hello(self):
... pass
>>>
>>> mark = User()
>>>
>>> type(mark.hello)
<class 'method'>
>>>
>>>
>>> hasattr(mark.hello, '__set_name__')
False
>>>
>>> hasattr(mark.hello, '__get__')
True
>>>
>>> hasattr(mark.hello, '__set__')
False
>>>
>>> hasattr(mark.hello, '__delete__')
False
Classes are Descriptors:
>>> class User:
... def hello(self):
... pass
>>>
>>> type(User.hello)
<class 'function'>
>>>
>>>
>>> hasattr(User.hello, '__set_name__')
False
>>>
>>> hasattr(User.hello, '__get__')
True
>>>
>>> hasattr(User.hello, '__set__')
False
>>>
>>> hasattr(User.hello, '__delete__')
False
9.4.6. Case Study 1
from unittest import TestCase
class User:
pass
class TestUser(TestCase):
def test_init(self):
user = User()
self.assertIsInstance(user, User)
from abc import ABC, abstractmethod
from dataclasses import dataclass
from unittest import TestCase
@dataclass
class Validator(ABC):
name: str = None
@property
@abstractmethod
def error(self) -> str: ...
@abstractmethod
def is_valid(self, value) -> bool:
raise NotImplementedError
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __delete__(self, instance):
instance.__dict__[self.name] = None
def __set__(self, instance, value):
if not self.is_valid(value):
fields = self.__dict__
error_message = self.error.format(**fields)
raise ValueError(error_message)
instance.__dict__[self.name] = value
@dataclass
class String(Validator):
max_length: int = 10
error: str = '{name} is longer than {max_length} characters'
def is_valid(self, value) -> bool:
return len(value) <= self.max_length
class User:
firstname = String(max_length=10)
class TestUser(TestCase):
def test_init(self):
user = User()
self.assertIsInstance(user, User)
class TestFirstname(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.firstname = 'Mark'
self.assertEqual(self.user.firstname, 'Mark')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.firstname = 'MarkMarkMarkMark'
from abc import ABC, abstractmethod
from dataclasses import dataclass
from unittest import TestCase
@dataclass
class Validator(ABC):
name: str = None
@property
@abstractmethod
def error(self) -> str: ...
@abstractmethod
def is_valid(self, value) -> bool:
raise NotImplementedError
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __delete__(self, instance):
instance.__dict__[self.name] = None
def __set__(self, instance, value):
if not self.is_valid(value):
fields = self.__dict__
error_message = self.error.format(**fields)
raise ValueError(error_message)
instance.__dict__[self.name] = value
@dataclass
class String(Validator):
max_length: int = 10
error: str = '{name} is longer than {max_length} characters'
def is_valid(self, value) -> bool:
return len(value) <= self.max_length
class User:
firstname = String(max_length=10)
lastname = String(max_length=15)
class TestUser(TestCase):
def test_init(self):
user = User()
self.assertIsInstance(user, User)
class TestFirstname(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.firstname = 'Mark'
self.assertEqual(self.user.firstname, 'Mark')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.firstname = 'MarkMarkMarkMark'
class TestLastname(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.lastname = 'Watney'
self.assertEqual(self.user.lastname, 'Watney')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.lastname = 'WatneyWatneyWatney'
from abc import ABC, abstractmethod
from dataclasses import dataclass
from unittest import TestCase
@dataclass
class Validator(ABC):
name: str = None
@property
@abstractmethod
def error(self) -> str: ...
@abstractmethod
def is_valid(self, value) -> bool:
raise NotImplementedError
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __delete__(self, instance):
instance.__dict__[self.name] = None
def __set__(self, instance, value):
if not self.is_valid(value):
fields = self.__dict__
error_message = self.error.format(**fields)
raise ValueError(error_message)
instance.__dict__[self.name] = value
@dataclass
class String(Validator):
max_length: int = 10
error: str = '{name} is longer than {max_length} characters'
def is_valid(self, value) -> bool:
return len(value) <= self.max_length
@dataclass
class Integer(Validator):
min: int = 0
max: int = 100
error: str = '{name} is not in range from {min} to {max}'
def is_valid(self, value) -> bool:
return value in range(self.min, self.max)
class User:
firstname = String(max_length=10)
lastname = String(max_length=15)
age = Integer(min=18, max=65)
class TestUser(TestCase):
def test_init(self):
user = User()
self.assertIsInstance(user, User)
class TestFirstname(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.firstname = 'Mark'
self.assertEqual(self.user.firstname, 'Mark')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.firstname = 'MarkMarkMarkMark'
class TestLastname(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.lastname = 'Watney'
self.assertEqual(self.user.lastname, 'Watney')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.lastname = 'WatneyWatneyWatney'
class TestAge(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.age = 40
self.assertEqual(self.user.age, 40)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.age = 17
with self.assertRaises(ValueError):
self.user.age = 66
from abc import ABC, abstractmethod
from dataclasses import dataclass
from unittest import TestCase
@dataclass
class Validator(ABC):
name: str = None
@property
@abstractmethod
def error(self) -> str: ...
@abstractmethod
def is_valid(self, value) -> bool:
raise NotImplementedError
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __delete__(self, instance):
instance.__dict__[self.name] = None
def __set__(self, instance, value):
if not self.is_valid(value):
fields = self.__dict__
error_message = self.error.format(**fields)
raise ValueError(error_message)
instance.__dict__[self.name] = value
@dataclass
class String(Validator):
max_length: int = 10
error: str = '{name} is longer than {max_length} characters'
def is_valid(self, value) -> bool:
return len(value) <= self.max_length
@dataclass
class Integer(Validator):
min: int = 0
max: int = 100
error: str = '{name} is not in range from {min} to {max}'
def is_valid(self, value) -> bool:
return value in range(self.min, self.max)
@dataclass
class Float(Validator):
min: float = 0.0
max: float = 100.0
error: str = '{name} is not between {min} and {max}'
def is_valid(self, value) -> bool:
return self.min <= value <= self.max
class User:
firstname = String(max_length=10)
lastname = String(max_length=15)
age = Integer(min=18, max=65)
height = Float(min=150.0, max=220.0)
class TestUser(TestCase):
def test_init(self):
user = User()
self.assertIsInstance(user, User)
class TestFirstname(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.firstname = 'Mark'
self.assertEqual(self.user.firstname, 'Mark')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.firstname = 'MarkMarkMarkMark'
class TestLastname(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.lastname = 'Watney'
self.assertEqual(self.user.lastname, 'Watney')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.lastname = 'WatneyWatneyWatney'
class TestAge(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.age = 40
self.assertEqual(self.user.age, 40)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.age = 17
with self.assertRaises(ValueError):
self.user.age = 66
class TestHeight(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.height = 175.0
self.assertEqual(self.user.height, 175.0)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.height = 149.5
with self.assertRaises(ValueError):
self.user.height = 220.5
from abc import ABC, abstractmethod
from dataclasses import dataclass
from unittest import TestCase
@dataclass
class Validator(ABC):
name: str = None
@property
@abstractmethod
def error(self) -> str: ...
@abstractmethod
def is_valid(self, value) -> bool:
raise NotImplementedError
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __delete__(self, instance):
instance.__dict__[self.name] = None
def __set__(self, instance, value):
if not self.is_valid(value):
fields = self.__dict__
error_message = self.error.format(**fields)
raise ValueError(error_message)
instance.__dict__[self.name] = value
@dataclass
class String(Validator):
max_length: int = 10
error: str = '{name} is longer than {max_length} characters'
def is_valid(self, value) -> bool:
return len(value) <= self.max_length
@dataclass
class Integer(Validator):
min: int = 0
max: int = 100
error: str = '{name} is not in range from {min} to {max}'
def is_valid(self, value) -> bool:
return value in range(self.min, self.max)
@dataclass
class Float(Validator):
min: float = 0.0
max: float = 100.0
error: str = '{name} is not between {min} and {max}'
def is_valid(self, value) -> bool:
return self.min <= value <= self.max
class User:
firstname = String(max_length=10)
lastname = String(max_length=15)
age = Integer(min=18, max=65)
height = Float(min=150.0, max=220.0)
weight = Float(min=50.0, max=120.0)
class TestUser(TestCase):
def test_init(self):
user = User()
self.assertIsInstance(user, User)
class TestFirstname(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.firstname = 'Mark'
self.assertEqual(self.user.firstname, 'Mark')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.firstname = 'MarkMarkMarkMark'
class TestLastname(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.lastname = 'Watney'
self.assertEqual(self.user.lastname, 'Watney')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.lastname = 'WatneyWatneyWatney'
class TestAge(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.age = 40
self.assertEqual(self.user.age, 40)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.age = 17
with self.assertRaises(ValueError):
self.user.age = 66
class TestHeight(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.height = 175.0
self.assertEqual(self.user.height, 175.0)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.height = 149.5
with self.assertRaises(ValueError):
self.user.height = 220.5
class TestWeight(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.weight = 75.0
self.assertEqual(self.user.weight, 75.0)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.weight = 49.5
with self.assertRaises(ValueError):
self.user.weight = 120.5
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from unittest import TestCase
@dataclass
class Validator(ABC):
name: str = None
@property
@abstractmethod
def error(self) -> str: ...
@abstractmethod
def is_valid(self, value) -> bool:
raise NotImplementedError
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __delete__(self, instance):
instance.__dict__[self.name] = None
def __set__(self, instance, value):
if not self.is_valid(value):
fields = self.__dict__
error_message = self.error.format(**fields)
raise ValueError(error_message)
instance.__dict__[self.name] = value
@dataclass
class String(Validator):
max_length: int = 10
error: str = '{name} is longer than {max_length} characters'
def is_valid(self, value) -> bool:
return len(value) <= self.max_length
@dataclass
class Integer(Validator):
min: int = 0
max: int = 100
error: str = '{name} is not in range from {min} to {max}'
def is_valid(self, value) -> bool:
return value in range(self.min, self.max)
@dataclass
class Float(Validator):
min: float = 0.0
max: float = 100.0
error: str = '{name} is not between {min} and {max}'
def is_valid(self, value) -> bool:
return self.min <= value <= self.max
@dataclass
class Literal(Validator):
choices: list = field(default_factory=list)
error: str = '{name} is not in {choices}'
def is_valid(self, value) -> bool:
return value in self.choices
class User:
firstname = String(max_length=10)
lastname = String(max_length=15)
age = Integer(min=18, max=65)
height = Float(min=150.0, max=220.0)
weight = Float(min=50.0, max=120.0)
group = Literal(choices=['users', 'staff', 'admins'])
class TestUser(TestCase):
def test_init(self):
user = User()
self.assertIsInstance(user, User)
class TestFirstname(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.firstname = 'Mark'
self.assertEqual(self.user.firstname, 'Mark')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.firstname = 'MarkMarkMarkMark'
class TestLastname(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.lastname = 'Watney'
self.assertEqual(self.user.lastname, 'Watney')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.lastname = 'WatneyWatneyWatney'
class TestAge(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.age = 40
self.assertEqual(self.user.age, 40)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.age = 17
with self.assertRaises(ValueError):
self.user.age = 66
class TestHeight(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.height = 175.0
self.assertEqual(self.user.height, 175.0)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.height = 149.5
with self.assertRaises(ValueError):
self.user.height = 220.5
class TestWeight(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.weight = 75.0
self.assertEqual(self.user.weight, 75.0)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.weight = 49.5
with self.assertRaises(ValueError):
self.user.weight = 120.5
class TestGroup(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.group = 'users'
self.assertEqual(self.user.group, 'users')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.group = 'invalid'
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from unittest import TestCase
@dataclass
class Validator(ABC):
name: str = None
@property
@abstractmethod
def error(self) -> str: ...
@abstractmethod
def is_valid(self, value) -> bool:
raise NotImplementedError
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __delete__(self, instance):
instance.__dict__[self.name] = None
def __set__(self, instance, value):
if not self.is_valid(value):
fields = self.__dict__
error_message = self.error.format(**fields)
raise ValueError(error_message)
instance.__dict__[self.name] = value
@dataclass
class String(Validator):
max_length: int = 10
error: str = '{name} is longer than {max_length} characters'
def is_valid(self, value) -> bool:
return len(value) <= self.max_length
@dataclass
class Integer(Validator):
min: int = 0
max: int = 100
error: str = '{name} is not in range from {min} to {max}'
def is_valid(self, value) -> bool:
return value in range(self.min, self.max)
@dataclass
class Float(Validator):
min: float = 0.0
max: float = 100.0
error: str = '{name} is not between {min} and {max}'
def is_valid(self, value) -> bool:
return self.min <= value <= self.max
@dataclass
class Literal(Validator):
choices: list = field(default_factory=list)
error: str = '{name} is not in {choices}'
def is_valid(self, value) -> bool:
return value in self.choices
@dataclass
class Email(Validator):
domain: str | tuple[str,...] = ''
error: str = '{name} is not in {domain}'
def is_valid(self, value) -> bool:
return value.endswith(self.domain)
class User:
firstname = String(max_length=10)
lastname = String(max_length=15)
age = Integer(min=18, max=65)
height = Float(min=150.0, max=220.0)
weight = Float(min=50.0, max=120.0)
group = Literal(choices=['users', 'staff', 'admins'])
email = Email(domain='nasa.gov')
class TestUser(TestCase):
def test_init(self):
user = User()
self.assertIsInstance(user, User)
class TestFirstname(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.firstname = 'Mark'
self.assertEqual(self.user.firstname, 'Mark')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.firstname = 'MarkMarkMarkMark'
class TestLastname(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.lastname = 'Watney'
self.assertEqual(self.user.lastname, 'Watney')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.lastname = 'WatneyWatneyWatney'
class TestAge(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.age = 40
self.assertEqual(self.user.age, 40)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.age = 17
with self.assertRaises(ValueError):
self.user.age = 66
class TestHeight(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.height = 175.0
self.assertEqual(self.user.height, 175.0)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.height = 149.5
with self.assertRaises(ValueError):
self.user.height = 220.5
class TestWeight(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.weight = 75.0
self.assertEqual(self.user.weight, 75.0)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.weight = 49.5
with self.assertRaises(ValueError):
self.user.weight = 120.5
class TestGroup(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.group = 'users'
self.assertEqual(self.user.group, 'users')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.group = 'invalid'
class TestEmail(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.email = 'mwatney@nasa.gov'
self.assertEqual(self.user.email, 'mwatney@nasa.gov')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.email = 'mwatney@nasa.gov.pl'
from abc import ABC, abstractmethod
from dataclasses import dataclass, field, InitVar
from unittest import TestCase
import re
@dataclass
class Validator(ABC):
name: str = None
@property
@abstractmethod
def error(self) -> str: ...
@abstractmethod
def is_valid(self, value) -> bool:
raise NotImplementedError
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __delete__(self, instance):
instance.__dict__[self.name] = None
def __set__(self, instance, value):
if not self.is_valid(value):
fields = self.__dict__
error_message = self.error.format(**fields)
raise ValueError(error_message)
instance.__dict__[self.name] = value
@dataclass
class String(Validator):
max_length: int = 10
error: str = '{name} is longer than {max_length} characters'
def is_valid(self, value) -> bool:
return len(value) <= self.max_length
@dataclass
class Integer(Validator):
min: int = 0
max: int = 100
error: str = '{name} is not in range from {min} to {max}'
def is_valid(self, value) -> bool:
return value in range(self.min, self.max)
@dataclass
class Float(Validator):
min: float = 0.0
max: float = 100.0
error: str = '{name} is not between {min} and {max}'
def is_valid(self, value) -> bool:
return self.min <= value <= self.max
@dataclass
class Literal(Validator):
choices: list = field(default_factory=list)
error: str = '{name} is not in {choices}'
def is_valid(self, value) -> bool:
return value in self.choices
@dataclass
class Email(Validator):
domain: str | tuple[str,...] = ''
error: str = '{name} is not in {domain}'
def is_valid(self, value) -> bool:
return value.endswith(self.domain)
@dataclass
class Phone(Validator):
regex: InitVar[str] = ''
pattern: re.Pattern = field(init=False, default=None)
error: str = '{name} does not match {pattern.pattern}'
def __post_init__(self, regex: str):
self.pattern = re.compile(regex)
def is_valid(self, value) -> bool:
return self.pattern.match(value) is not None
class User:
firstname = String(max_length=10)
lastname = String(max_length=15)
age = Integer(min=18, max=65)
height = Float(min=150.0, max=220.0)
weight = Float(min=50.0, max=120.0)
group = Literal(choices=['users', 'staff', 'admins'])
email = Email(domain='nasa.gov')
phone = Phone(regex=r'^\+\d{1,4} \(\d{3}\) \d{3}-\d{3,4}$')
class TestUser(TestCase):
def test_init(self):
user = User()
self.assertIsInstance(user, User)
class TestFirstname(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.firstname = 'Mark'
self.assertEqual(self.user.firstname, 'Mark')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.firstname = 'MarkMarkMarkMark'
class TestLastname(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.lastname = 'Watney'
self.assertEqual(self.user.lastname, 'Watney')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.lastname = 'WatneyWatneyWatney'
class TestAge(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.age = 40
self.assertEqual(self.user.age, 40)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.age = 17
with self.assertRaises(ValueError):
self.user.age = 66
class TestHeight(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.height = 175.0
self.assertEqual(self.user.height, 175.0)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.height = 149.9
with self.assertRaises(ValueError):
self.user.height = 220.1
class TestWeight(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.weight = 75.0
self.assertEqual(self.user.weight, 75.0)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.weight = 49.9
with self.assertRaises(ValueError):
self.user.weight = 120.1
class TestGroup(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.group = 'users'
self.assertEqual(self.user.group, 'users')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.group = 'invalid'
class TestEmail(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.email = 'mwatney@nasa.gov'
self.assertEqual(self.user.email, 'mwatney@nasa.gov')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.email = 'mwatney@nasa.gov.pl'
class TestPhone(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.phone = '+1 (234) 567-8910'
self.assertEqual(self.user.phone, '+1 (234) 567-8910')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.phone = '+1 234 567-8910'
with self.assertRaises(ValueError):
self.user.phone = '+1 (234) 567 8910'
with self.assertRaises(ValueError):
self.user.phone = '+12345678910'
with self.assertRaises(ValueError):
self.user.phone = '+1 2345678910'
with self.assertRaises(ValueError):
self.user.phone = '(234) 567-8910'
9.4.7. Case Study 2
from abc import ABC, abstractmethod
from dataclasses import dataclass, field, InitVar
import re
@dataclass
class Validator(ABC):
name: str = None
@property
@abstractmethod
def error(self) -> str: ...
@abstractmethod
def is_valid(self, value) -> bool:
raise NotImplementedError
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __delete__(self, instance):
instance.__dict__[self.name] = None
def __set__(self, instance, value):
if not self.is_valid(value):
fields = self.__dict__
error_message = self.error.format(**fields)
raise ValueError(error_message)
instance.__dict__[self.name] = value
@dataclass
class String(Validator):
max_length: int = 10
error: str = '{name} is longer than {max_length} characters'
def is_valid(self, value) -> bool:
return len(value) <= self.max_length
@dataclass
class Integer(Validator):
min: int = 0
max: int = 100
error: str = '{name} is not in range from {min} to {max}'
def is_valid(self, value) -> bool:
return value in range(self.min, self.max)
@dataclass
class Float(Validator):
min: float = 0.0
max: float = 100.0
error: str = '{name} is not between {min} and {max}'
def is_valid(self, value) -> bool:
return self.min <= value <= self.max
@dataclass
class Literal(Validator):
choices: list = field(default_factory=list)
error: str = '{name} is not in {choices}'
def is_valid(self, value) -> bool:
return value in self.choices
@dataclass
class Email(Validator):
domain: str | tuple[str,...] = ''
error: str = '{name} is not in {domain}'
def is_valid(self, value) -> bool:
return value.endswith(self.domain)
@dataclass
class Phone(Validator):
regex: InitVar[str] = ''
pattern: re.Pattern = field(init=False, default=None)
error: str = '{name} does not match {pattern.pattern}'
def __post_init__(self, regex: str):
self.pattern = re.compile(regex)
def is_valid(self, value) -> bool:
return self.pattern.match(value) is not None
9.4.8. Case Study 3
from dataclasses import dataclass
@dataclass
class Group:
name: str
age_min: int
age_max: int
@dataclass
class User:
firstname: str
lastname: str
age: int
group: list[Group]
def __post_init__(self):
for group in self.group:
if not group.age_min <= self.age <= group.age_max:
error = (f'{self.firstname} {self.lastname} is not in the '
f'age range {group.age_min} to {group.age_max}'
f' for group {group.name}')
raise ValueError(error)
users = Group('users', age_min=0, age_max=99)
staff = Group('users', age_min=10, age_max=99)
admins = Group('admins', age_min=18, age_max=65)
mark = User('Mark', 'Watney', age=40, group=[users, staff])
melissa = User('Melissa', 'Lewis', age=44, group=[users, staff, admins])
rick = User('Rick', 'Martinez', age=39, group=[users, staff])
# alex = User('Alex', 'Vogel', age=10, group=[users, admins])
# Traceback (most recent call last):
# ValueError: Alex Vogel is not in the age range 18 to 65 for group admins
9.4.9. Use Case - 1
Kelvin Temperature Validator
>>> class KelvinValidator:
... def __set__(self, instance, value):
... if value < 0.0:
... raise ValueError('Cannot set negative Kelvin')
... instance.__dict__['value'] = value
...
... def __get__(self, instance, owner):
... return instance.__dict__['value']
>>>
>>>
>>> class Temperature:
... kelvin = KelvinValidator()
>>>
>>>
>>> t = Temperature()
>>>
>>> t.kelvin = 273.15
>>> print(t.kelvin)
273.15
>>>
>>> t.kelvin = -1
Traceback (most recent call last):
ValueError: Cannot set negative Kelvin
9.4.10. Use Case - 2
Temperature Conversion
>>> class Kelvin:
... def __get__(self, instance, owner):
... kelvin = instance.__dict__['absolute']
... return round(kelvin, 2)
...
... def __set__(self, instance, kelvin):
... instance.__dict__['absolute'] = kelvin
>>>
>>>
>>> class Celsius:
... def __get__(self, instance, owner):
... kelvin = instance.__dict__['absolute']
... celsius = kelvin - 273.15
... return round(celsius, 2)
...
... def __set__(self, instance, celsius):
... kelvin = celsius + 273.15
... instance.__dict__['absolute'] = kelvin
>>>
>>>
>>> class Fahrenheit:
... def __get__(self, instance, owner):
... kelvin = instance.__dict__['absolute']
... fahrenheit = (kelvin-273.15) * (9/5) + 32
... return round(fahrenheit, 2)
...
... def __set__(self, instance, fahrenheit):
... kelvin = (fahrenheit-32) * (5/9) + 273.15
... instance.__dict__['absolute'] = kelvin
>>>
>>>
>>> class Temperature:
... kelvin = Kelvin()
... celsius = Celsius()
... fahrenheit = Fahrenheit()
>>>
>>>
>>> t = Temperature()
>>>
>>> t.kelvin = 273.15
>>> print(t.kelvin)
273.15
>>> print(t.celsius)
0.0
>>> print(t.fahrenheit)
32.0
>>>
>>> t.fahrenheit = 100
>>> print(t.kelvin)
310.93
>>> print(t.celsius)
37.78
>>> print(t.fahrenheit)
100.0
>>>
>>> t.celsius = 100
>>> print(t.kelvin)
373.15
>>> print(t.celsius)
100.0
>>> print(t.fahrenheit)
212.0
9.4.11. Use Case - 3
Value Range Descriptor
>>> class Between:
... def __init__(self, min, max):
... self.min = min
... self.max = max
...
... def __set_name__(self, owner, name):
... self.name = name
...
... def __get__(self, instance, owner):
... return instance.__dict__[self.name]
...
... def __set__(self, instance, value):
... if not self.min <= value <= self.max:
... raise ValueError(f'Value of field "{self.name}" is not between {self.min} and {self.max}')
... instance.__dict__[self.name] = value
...
... def __delete__(self, instance):
... instance.__dict__[self.name] = None
>>>
>>>
>>> class User:
... firstname: str
... lastname: str
... age = Between(30, 50)
... height = Between(150, 210)
... weight = Between(50, 90)
>>> mark = User()
>>>
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>> mark.age = 42
>>> mark.height = 178.0
>>> mark.weight = 75.5
>>> mark.age = 18
Traceback (most recent call last):
ValueError: Value of field "age" is not between 30 and 50
>>>
>>> mark.weight = 100
Traceback (most recent call last):
ValueError: Value of field "weight" is not between 50 and 90
>>>
>>> mark.height = 220
Traceback (most recent call last):
ValueError: Value of field "height" is not between 150 and 210
9.4.12. Use Case - 5
>>> from abc import ABC, abstractmethod
>>> from dataclasses import InitVar, dataclass, field
>>> from typing import Any
>>> import re
>>>
>>>
>>> @dataclass
... class Validator(ABC):
... name: str = field(init=False, repr=False)
...
... @property
... @abstractmethod
... def error(self) -> str:
... raise NotImplementedError
...
... @abstractmethod
... def is_valid(self, value: Any) -> bool:
... raise NotImplementedError
...
... def __set_name__(self, owner, name):
... self.name = name
...
... def __get__(self, instance, owner):
... return instance.__dict__[self.name]
...
... def __set__(self, instance, value):
... if not self.is_valid(value):
... raise ValueError(self.error.format(**vars(self), value=value))
... instance.__dict__[self.name] = value
...
... def __delete__(self, instance):
... instance.__dict__[self.name] = None
...
...
>>> @dataclass
... class String(Validator):
... max_length: int
... error: str = 'Field `{name}` value `{value}` is longer than {max_length}'
...
... def is_valid(self, value) -> bool:
... return len(value) <= self.max_length
...
...
>>> @dataclass
... class Integer(Validator):
... min: int
... max: int
... error: str = 'Field `{name}` value `{value}` is not in between {min} and {max}'
...
... def is_valid(self, value) -> bool:
... return value in range(self.min, self.max)
...
...
>>> @dataclass
... class Select(Validator):
... options: list[str]
... error: str = 'Field `{name}` value `value` not in {options}'
...
... def is_valid(self, value: Any) -> bool:
... return value in self.options
...
>>> @dataclass
... class Email(Validator):
... domain: str
... error: str = 'Field `{name}` value `{value}` does not ends with `{domain}`'
...
... def is_valid(self, value) -> bool:
... return value.endswith(self.domain)
...
...
>>> @dataclass
... class Regex(Validator):
... pattern: InitVar[str]
... regex: re.Pattern | None = field(default=None, init=False)
... error: str = 'Field `{name}` value `{value}` does not match regex `{regex.pattern}`'
...
... def __post_init__(self, pattern: str):
... self.regex = re.compile(pattern)
...
... def is_valid(self, value: Any) -> bool:
... return self.regex.match(value)
>>> class User:
... firstname = String(max_length=10)
... lastname = String(max_length=15)
... age = Integer(min=0, max=130)
... group = Select(options=['user', 'staff', 'admin'])
... email = Email(domain='@nasa.gov')
... phone = Regex(pattern=r'^\+1 \(\d{3}\) \d{3}-\d{4}$')
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>> mark.email = 'mwatney@nasa.gov'
>>> mark.age = 41
>>> mark.group = 'user'
>>> mark.phone = '+1 (234) 567-8910'
>>>
>>> vars(mark)
{'firstname': 'Mark',
'lastname': 'Watney',
'email': 'mwatney@nasa.gov',
'age': 41,
'group': 'user',
'phone': '+1 (234) 567-8910'}
>>>
>>> mark.firstname
'Mark'
>>> mark.lastname
'Watney'
>>> mark.email
'mwatney@nasa.gov'
>>> mark.age
41
>>> mark.group
'user'
>>> mark.phone
'+1 (234) 567-8910'
>>> mark.firstname = 'MarkMarkMarkMark'
Traceback (most recent call last):
ValueError: Field `firstname` value `MarkMarkMarkMark` is longer than 10
>>> mark.lastname = 'WatneyWatneyWatneyWatneyWatney'
Traceback (most recent call last):
ValueError: Field `lastname` value `WatneyWatneyWatneyWatneyWatney` is longer than 15
>>> mark.age = 135
Traceback (most recent call last):
ValueError: Field `age` value `135` is not in between 0 and 130
>>> mark.group = 'editors'
Traceback (most recent call last):
ValueError: Field `group` value `value` not in ['user', 'staff', 'admin']
>>> mark.email = 'mwatney@gmail.com'
Traceback (most recent call last):
ValueError: Field `email` value `mwatney@gmail.com` does not ends with `@nasa.gov`
>>> mark.phone = '+48 123 456 789'
Traceback (most recent call last):
ValueError: Field `phone` value `+48 123 456 789` does not match regex `^\+1 \(\d{3}\) \d{3}-\d{4}$`
>>> vars(User)
mappingproxy({'__module__': '__main__',
'__firstlineno__': 1,
'firstname': String(max_length=10, error='Field `{name}` value `{value}` is longer than {max_length}'),
'lastname': String(max_length=15, error='Field `{name}` value `{value}` is longer than {max_length}'),
'age': Integer(min=0, max=130, error='Field `{name}` value `{value}` is not in between {min} and {max}'),
'group': Select(options=['user', 'staff', 'admin'], error='Field `{name}` value `value` not in {options}'),
'email': Email(domain='@nasa.gov', error='Field `{name}` value `{value}` does not ends with `{domain}`'),
'phone': Regex(regex=re.compile('^\\+1 \\(\\d{3}\\) \\d{3}-\\d{4}$'), error='Field `{name}` value `{value}` does not match regex `{regex.pattern}`'),
'__static_attributes__': (),
'__dict__': <attribute '__dict__' of 'User' objects>,
'__weakref__': <attribute '__weakref__' of 'User' objects>,
'__doc__': None})
9.4.13. Use Case - 6
Timezone Converter Descriptor
Descriptor Timezone Converter:
>>> from dataclasses import dataclass
>>> from datetime import datetime
>>> from zoneinfo import ZoneInfo
>>>
>>>
>>> class Timezone:
... def __init__(self, name):
... self.timezone = ZoneInfo(name)
...
... def __get__(self, instance, owner):
... utc = instance.utc.replace(tzinfo=ZoneInfo('UTC'))
... return utc.astimezone(self.timezone)
...
... def __set__(self, instance, value):
... local_time = value.replace(tzinfo=self.timezone)
... instance.utc = local_time.astimezone(ZoneInfo('UTC'))
>>>
>>>
>>> @dataclass
... class Time:
... utc = datetime.now(tz=ZoneInfo('UTC'))
... warsaw = Timezone('Europe/Warsaw')
... eastern = Timezone('America/New_York')
... pacific = Timezone('America/Los_Angeles')
>>> # Gagarin's launch to space
>>> gagarin = Time()
>>>
>>> gagarin.utc = datetime(1961, 4, 12, 6, 7)
>>>
>>> print(gagarin.utc)
1961-04-12 06:07:00
>>>
>>> print(gagarin.warsaw)
1961-04-12 07:07:00+01:00
>>>
>>> print(gagarin.eastern)
1961-04-12 01:07:00-05:00
>>>
>>> print(gagarin.pacific)
1961-04-11 22:07:00-08:00
>>> # Armstrong's first Lunar step
>>> armstrong = Time()
>>>
>>> armstrong.warsaw = datetime(1969, 7, 21, 3, 56, 15)
>>>
>>> print(armstrong.utc)
1969-07-21 02:56:15+00:00
>>>
>>> print(armstrong.warsaw)
1969-07-21 03:56:15+01:00
>>>
>>> print(armstrong.eastern)
1969-07-20 22:56:15-04:00
>>>
>>> print(armstrong.pacific)
1969-07-20 19:56:15-07:00
9.4.14. Assignments
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: Accessor Descriptor Simple
# - Difficulty: easy
# - Lines: 9
# - Minutes: 5
# %% English
# 1. Define descriptor class `Kelvin`
# 2. Temperature must always be positive
# 3. Use descriptors to check boundaries at each value modification
# 4. All tests must pass
# 5. Run doctests - all must succeed
# %% Polish
# 1. Zdefiniuj klasę deskryptor `Kelvin`
# 2. Temperatura musi być zawsze być dodatnia
# 3. Użyj deskryptorów do sprawdzania zakresów przy każdej modyfikacji
# 4. Wszystkie testy muszą przejść
# 5. Uruchom doctesty - wszystkie muszą się powieść
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> class Temperature:
... kelvin = Kelvin()
>>> t = Temperature()
>>> t.kelvin = 1
>>> t.kelvin
1
>>> t.kelvin = -1
Traceback (most recent call last):
ValueError: Negative temperature
"""
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: Accessor Descriptor ValueRange
# - Difficulty: medium
# - Lines: 9
# - Minutes: 13
# %% English
# 1. Define descriptor class `ValueRange` with attributes:
# - `name: str`
# - `min: float`
# - `max: float`
# 2. Define class `User` with attributes:
# - `firstname: str`
# - `lastname: str`
# - `age: int` range from 18 to 65
# - `height: float` range from 150 to 200
# - `weight: float` range from 50 to 150
# 3. Setting `User` attribute should invoke boundary check
# 4. Run doctests - all must succeed
# %% Polish
# 1. Zdefiniuj klasę-deskryptor `ValueRange` z atrybutami:
# - `name: str`
# - `min: float`
# - `max: float`
# 2. Zdefiniuj klasę `User` z atrybutami:
# - `firstname: str`
# - `lastname: str`
# - `age: int` zakres od 18 do 65
# - `height: float` zakres od 150 do 200
# - `weight: float` zakres od 50 do 150
# 3. Ustawianie atrybutu `User` powinno wywołać sprawdzanie zakresu
# 4. Uruchom doctesty - wszystkie muszą się powieść
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> mark = User('Mark', 'Watney', age=36, height=170, weight=80)
>>> melissa = User('Melissa', 'Lewis', age=66, height=170, weight=80)
Traceback (most recent call last):
ValueError: age is not between 18 and 65
>>> rick = User('Rick', 'Martinez', age=39, height=201, weight=75)
Traceback (most recent call last):
ValueError: height is not between 150 and 200
>>> alex = User('Alex', 'Vogel', age=41, height=175, weight=151)
Traceback (most recent call last):
ValueError: weight is not between 50 and 150
"""
# Define descriptor class `ValueRange` with attributes:
# - `name: str`
# - `min: float`
# - `max: float`
# type: type[ValueRange]
class ValueRange:
name: str
min: float
max: float
# Define class `User` with attributes:
# - `firstname: str`
# - `lastname: str`
# - `age: int` range from 18 to 65
# - `height: float` range from 150 to 200
# - `weight: float` range from 50 to 150
# Setting `User` attribute should invoke boundary check
# type: type[User]
class User:
firstname: str
lastname: str
age: int
height: float
weight: float
def __init__(self, firstname, lastname, age, height, weight):
self.firstname = firstname
self.lastname = lastname
self.age = age
self.height = height
self.weight = weight
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: Accessor Descriptor Inheritance
# - Difficulty: medium
# - Lines: 23
# - Minutes: 13
# %% English
# 1. Define class `GeographicCoordinate`
# 2. Use descriptors to check value boundaries
# 3. Run doctests - all must succeed
# %% Polish
# 1. Zdefiniuj klasę `GeographicCoordinate`
# 2. Użyj deskryptory do sprawdzania wartości brzegowych
# 3. Uruchom doctesty - wszystkie muszą się powieść
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> place1 = GeographicCoordinate(50.0, 120.0, 8000.0)
>>> place1
GeographicCoordinate(latitude=50.0, longitude=120.0, elevation=8000.0)
>>> place2 = GeographicCoordinate(22.0, 33.0, 44.0)
>>> place2
GeographicCoordinate(latitude=22.0, longitude=33.0, elevation=44.0)
>>> place1.latitude = 1.0
>>> place1.longitude = 2.0
>>> place1
GeographicCoordinate(latitude=1.0, longitude=2.0, elevation=8000.0)
>>> place2
GeographicCoordinate(latitude=22.0, longitude=33.0, elevation=44.0)
>>> GeographicCoordinate(90.0, 0.0, 0.0)
GeographicCoordinate(latitude=90.0, longitude=0.0, elevation=0.0)
>>> GeographicCoordinate(-90.0, 0.0, 0.0)
GeographicCoordinate(latitude=-90.0, longitude=0.0, elevation=0.0)
>>> GeographicCoordinate(0.0, +180.0, 0.0)
GeographicCoordinate(latitude=0.0, longitude=180.0, elevation=0.0)
>>> GeographicCoordinate(0.0, -180.0, 0.0)
GeographicCoordinate(latitude=0.0, longitude=-180.0, elevation=0.0)
>>> GeographicCoordinate(0.0, 0.0, +8848.0)
GeographicCoordinate(latitude=0.0, longitude=0.0, elevation=8848.0)
>>> GeographicCoordinate(0.0, 0.0, -10994.0)
GeographicCoordinate(latitude=0.0, longitude=0.0, elevation=-10994.0)
>>> GeographicCoordinate(-91.0, 0.0, 0.0)
Traceback (most recent call last):
ValueError: Out of bounds, must be between -90.0 and 90.0
>>> GeographicCoordinate(+91.0, 0.0, 0.0)
Traceback (most recent call last):
ValueError: Out of bounds, must be between -90.0 and 90.0
>>> GeographicCoordinate(0.0, -181.0, 0.0)
Traceback (most recent call last):
ValueError: Out of bounds, must be between -180.0 and 180.0
>>> GeographicCoordinate(0.0, +181.0, 0.0)
Traceback (most recent call last):
ValueError: Out of bounds, must be between -180.0 and 180.0
>>> GeographicCoordinate(0.0, 0.0, -10995.0)
Traceback (most recent call last):
ValueError: Out of bounds, must be between -10994.0 and 8848.0
>>> GeographicCoordinate(0.0, 0.0, +8849.0)
Traceback (most recent call last):
ValueError: Out of bounds, must be between -10994.0 and 8848.0
"""
from dataclasses import dataclass
class Latitude:
min = -90.0
max = 90.0
class Longitude:
min = -180.0
max = 180.0
class Elevation:
min = -10994.0
max = 8848.0
@dataclass
class GeographicCoordinate:
latitude: float = Latitude()
longitude: float = Longitude()
elevation: float = Elevation()