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
9.4.7. Case Study 2
File myapp/myframework.py
:
import re
from abc import ABC, abstractmethod
from dataclasses import InitVar, dataclass, field
class Validator(ABC):
name: str
@property
@abstractmethod
def error_message(self) -> str:
raise NotImplementedError
@abstractmethod
def is_valid(self, value) -> bool:
raise NotImplementedError
def __set_name__(self, owner, name):
self.name = name
def __set__(self, instance, value):
if not self.is_valid(value):
error_msg = self.error_message.format(**self.__dict__)
raise ValueError(error_msg)
instance.__dict__[self.name] = value
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __delete__(self, instance):
instance.__dict__[self.name] = None
@dataclass
class String(Validator):
max_length: int
error_message: str = '{name} is longer than {max_length}'
def is_valid(self, value):
return len(value) <= self.max_length
@dataclass
class Integer(Validator):
min: int
max: int
error_message: str = '{name} is not between {min} and {max}'
def is_valid(self, value):
return value in range(self.min, self.max+1)
@dataclass
class Float(Validator):
min: int
max: int
error_message: str = '{name} is not between {min} and {max}'
def is_valid(self, value):
return self.min <= value <= self.max
@dataclass
class Literal(Validator):
choices: tuple[str,...]
error_message: str = '{name} is not in {choices}'
def is_valid(self, value):
return value in self.choices
@dataclass
class Email(Validator):
domains: tuple[str,...]
error_message: str = '{name} is not in {domains}'
def is_valid(self, value):
if value.count('@') != 1:
return False
username, domain = value.split('@')
return domain in self.domains
@dataclass
class Phone(Validator):
pattern: InitVar[str]
regex: re.Pattern = field(init=False)
error_message: str = '{name} does not match {regex.pattern}'
def __post_init__(self, pattern: str):
self.regex = re.compile(pattern)
def is_valid(self, value) -> bool:
match = self.regex.match(value)
return match is not None
File myapp/main.py
:
from dataclasses import dataclass
from myframework import String, Integer, Float, Email, Phone, Literal
@dataclass
class User:
firstname: str = String(max_length=10)
lastname: str = String(max_length=15)
age: int = Integer(min=0, max=130)
height: float = Float(min=100, max=250)
weight: float = Float(min=0, max=250)
group: str = Literal(choices=('users', 'staff', 'admins'))
email: str = Email(domains=('nasa.gov', 'esa.int'))
phone: str = Phone(pattern=r'^\+\d{1,4} \(\d{3}\) \d{3}-\d{4}$')
mark = User(
firstname='Mark',
lastname='Watney',
age=41,
height=175.0,
weight=85.0,
group='users',
email='mwatney@nasa.gov',
phone='+1 (123) 456-7890',
)
print(mark)
File myapp/tests.py
:
from unittest import TestCase
from main import User
class TestUser(TestCase):
def test_user_create(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 = 'WatneyWatneyWatneyWatney'
class TestAge(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.age = 41
self.assertEqual(self.user.age, 41)
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.age = -1
with self.assertRaises(ValueError):
self.user.age = 131
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 = 99.99
with self.assertRaises(ValueError):
self.user.height = 250.01
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 = -0.01
with self.assertRaises(ValueError):
self.user.weight = 250.01
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 = 'user'
with self.assertRaises(ValueError):
self.user.group = 'not-existing'
class TestEmail(TestCase):
def setUp(self):
self.user = User()
def test_valid(self):
self.user.email = 'mwantey@nasa.gov'
self.assertEqual(self.user.email, 'mwantey@nasa.gov')
def test_invalid(self):
with self.assertRaises(ValueError):
self.user.email = 'mwantey@nasa.gov.pl'
with self.assertRaises(ValueError):
self.user.email = 'mwantey@roscosmos.ru'
with self.assertRaises(ValueError):
self.user.email = 'mwanteyroscosmos.ru'
with self.assertRaises(ValueError):
self.user.email = 'mwan@tey@nasa.gov'
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 = '12345678910'
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

Figure 9.2. Comparing datetime works only when all has the same timezone (UTC). More information in Stdlib Datetime Timezone
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
# %% About
# - Name: Accessor Descriptor Simple
# - Difficulty: easy
# - Lines: 9
# - Minutes: 5
# %% 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
# %% 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ść
# %% Doctests
"""
>>> 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
"""
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% Imports
# %% Types
# %% Data
# %% Result
# %% About
# - Name: Accessor Descriptor ValueRange
# - Difficulty: medium
# - Lines: 9
# - Minutes: 13
# %% 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
# %% 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ść
# %% Doctests
"""
>>> 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
"""
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% Imports
from dataclasses import dataclass
# %% Types
ValueRange: type
User: type
# %% Data
# %% Result
@dataclass
class ValueRange:
min: float
max: float
@dataclass
class User:
firstname: str
lastname: str
age: int
height: float
weight: float
# %% About
# - Name: Accessor Descriptor Inheritance
# - Difficulty: medium
# - Lines: 23
# - Minutes: 13
# %% 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
# %% 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ść
# %% Doctests
"""
>>> 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
"""
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% Imports
from dataclasses import dataclass
# %% Types
Validator: type
# %% Data
# %% Result
class Validator:
min: float
max: float
class Latitude(Validator):
min = -90.0
max = 90.0
class Longitude(Validator):
min = -180.0
max = 180.0
class Elevation(Validator):
min = -10994.0
max = 8848.0
@dataclass
class GeographicCoordinate:
latitude: float = Latitude()
longitude: float = Longitude()
elevation: float = Elevation()