19.5. OOP Property

  • Disable attribute modification

  • Logging value access

In Python, @property is a built-in decorator that allows you to define a method as a read-only property of a class. This means that the method can be accessed like an attribute, without needing to call it as a method. The @property decorator is used to define a getter method for a property. The getter method should have the same name as the property, and it should return the value of the property.

Here's an example of using the @property decorator to define a read-only property of a class:

>>> class MyClass:
...     def __init__(self, x):
...         self._x = x
...
...     @property
...     def x(self):
...         return self._x
>>>
>>> # Create an instance of MyClass
>>> obj = MyClass(10)
>>>
>>> # Access the property like an attribute
>>> print(obj.x)
10

In this example, the MyClass class defines a private attribute _x and a getter method x() decorated with the @property decorator. The x() method returns the value of the _x attribute.

The obj.x expression accesses the x property of the obj instance of MyClass, which calls the x() method to retrieve the value of the _x attribute. Note that the _x attribute is private and cannot be accessed directly from outside the class.

19.5.1. Problem

>>> class User:
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
...
...     def username(self):
...         return f'{self.firstname[0]}{self.lastname}'.lower()
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>>
>>> mark.firstname
'Mark'
>>>
>>> mark.lastname
'Watney'
>>>
>>> mark.username()
'mwatney'

19.5.2. Solution

>>> class User:
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
...
...     @property
...     def username(self):
...         return f'{self.firstname[0]}{self.lastname}'.lower()
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>>
>>> mark.firstname
'Mark'
>>>
>>> mark.lastname
'Watney'
>>>
>>> mark.username
'mwatney'

19.5.3. Cached Property

>>> from functools import cached_property
>>>
>>> class User:
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
...
...     @cached_property
...     def username(self):
...         return f'{self.firstname[0]}{self.lastname}'.lower()
>>> mark = User('Mark', 'Watney')
>>>
>>> mark.firstname
'Mark'
>>>
>>> mark.lastname
'Watney'
>>>
>>> mark.username
'mwatney'

19.5.4. Use Case - 1

>>> from datetime import date
>>>
>>>
>>> YEAR = 365.25
>>>
>>> class User:
...     def __init__(self, firstname, lastname, birthdate):
...         self.firstname = firstname
...         self.lastname = lastname
...         self.birthdate = birthdate
...
...     @property
...     def age(self):
...         diff = date.today() - self.birthdate
...         return int(diff.days/YEAR)
>>>
>>>
>>> mark = User('Mark', 'Watney', birthdate=date(2000, 1, 2))
>>> mark.age
25

19.5.5. Use Case - 2

>>> class User:
...     def __init__(self, firstname, lastname):
...         self._firstname = firstname
...         self._lastname = lastname
...
...     @property
...     def name(self):
...         return f'{self._firstname} {self._lastname[0]}.'
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>> print(mark.name)
Mark W.

19.5.6. Use Case - 3

>>> import logging
>>>
>>>
>>> class User:
...     def __init__(self, username, password):
...         self._username = username
...         self._password = password
...
...     @property
...     def username(self):
...         logging.warning("User's username was accessed")
...         return self._username
...
...     @property
...     def password(self):
...         logging.warning("User's password was accessed")
...         return self._password
>>>
>>>
>>> mark = User(username='mwatney', password='Ares3')
>>>
>>> print(mark.password)
Ares3

19.5.7. Use Case - 4

>>> class Point:
...     x: int
...     y: int
...     z: int
...
...     @property
...     def position(self):
...         return self.x, self.y, self.z
>>>
>>>
>>> pt = Point()
>>> pt.x = 1
>>> pt.y = 2
>>> pt.z = 3
>>>
>>> print(pt.position)
(1, 2, 3)

19.5.8. Use Case - 5

>>> from datetime import date
>>>
>>> YEAR = 365.25
>>>
>>>
>>> class User:
...     def __init__(self, firstname, lastname, birthdate):
...         self.firstname = firstname
...         self.lastname = lastname
...         self.birthdate = birthdate
...
...     @property
...     def age(self):
...         td = date.today() - self.birthdate
...         return int(td.days / YEAR)
...
...     @property
...     def fullname(self):
...         return f'{self.firstname} {self.lastname}'
...
...     @property
...     def gdpr(self):
...         return f'{self.firstname} {self.lastname[0]}. ({self.age} years)'
>>>
>>>
>>> mark = User('Mark', 'Watney', birthdate=date(2000, 1, 2))
>>>
>>> mark.firstname
'Mark'
>>>
>>> mark.lastname
'Watney'
>>>
>>> mark.birthdate
datetime.date(2000, 1, 2)
>>>
>>> mark.age
25
>>>
>>> mark.fullname
'Mark Watney'
>>>
>>> mark.gdpr
'Mark W. (25 years)'

19.5.9. 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: OOP Property Getter
# - Difficulty: easy
# - Lines: 4
# - Minutes: 3

# %% English
# 1. Define property `position` in class `Point`
# 2. Accessing `position` returns tuple `(x, y, z)`
# 3. Run doctests - all must succeed

# %% Polish
# 1. Zdefiniuj property `position` w klasie `Point`
# 2. Dostęp do `position` zwraca tuplę `(x, y, z)`
# 3. Uruchom doctesty - wszystkie muszą się powieść

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> pt = Point(x=1, y=2, z=3)
>>> pt.x, pt.y, pt.z
(1, 2, 3)
>>> pt.position
(1, 2, 3)
"""

# Define property `position` in class `Point`
# Accessing `position` returns tuple `(x, y, z)`
# type: type[Point]
class Point:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z


# %% 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: OOP Property Age
# - Difficulty: easy
# - Lines: 5
# - Minutes: 5

# %% English
# 1. Define property `age` in class `User`
# 2. Accessing `age` should return user's age in full years
# 3. Run doctests - all must succeed

# %% Polish
# 1. Zdefiniuj property `age` w klasie `User`
# 2. Dostęp do `age` powinien zwrócić wiek użytkownika w pełnych latach
# 3. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `date.today()`
# - `timedelta.days`
# - `int()`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from datetime import date
>>> age = date.today().year - 2000

>>> mark = User(
...     firstname='Mark',
...     lastname='Watney',
...     birthdate=date(2000, 1, 1))

>>> assert hasattr(mark, 'age'), \
'Define property `age`'

>>> assert mark.age == age, \
f'Invalid age "{mark.age}", should be "{age}"'
"""

from datetime import date

YEAR = 365.25

# Define property `age` in class `User`
# Accessing `age` should return user's age in full years
# type: Callable[[Self], int]
class User:
    def __init__(self, firstname, lastname, birthdate):
        self.firstname = firstname
        self.lastname = lastname
        self.birthdate = birthdate


# %% 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: OOP Property NumericValues
# - Difficulty: easy
# - Lines: 2
# - Minutes: 5

# %% English
# 1. Define property `numeric_values` in class `Iris`
# 2. Accessing `numeric_values` should return a tuple
#    with all numeric attribute values
# 3. Run doctests - all must succeed

# %% Polish
# 1. Zdefiniuj property `numeric_values` w klasie `Iris`
# 2. Dostęp do `numeric_values` powinien zwrócić tuplę
#    z wszystkimi wartościami atrybutów numerycznych
# 3. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `var(self)`
# - `dict.values()`
# - `instanceof()`
# - `type()`
# - `@property`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from inspect import isfunction

>>> assert hasattr(Iris, '__init__')
>>> assert hasattr(Iris, 'numeric_values')
>>> assert not isfunction(Iris.numeric_values)

>>> assert Iris.numeric_values.__class__ is property
>>> assert Iris.numeric_values.fdel is None
>>> assert Iris.numeric_values.fset is None
>>> assert Iris.numeric_values.fget is not None

>>> setosa = Iris(5.1, 3.5, 1.4, 0.2, 'setosa')
>>> setosa.numeric_values
(5.1, 3.5, 1.4, 0.2)
"""


class Iris:
    def __init__(self, sl, sw, pl, pw, species):
        self.sepal_length = sl
        self.sepal_width = sw
        self.petal_length = pl
        self.petal_width = pw
        self.species = species

    # Create property `numeric_values`,
    # which returns a tuple of values of all `float` type attributes
    # type: Callable[[], tuple[float]]
    def numeric_values(self):
        ...


# %% 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: OOP Property NumericValues
# - Difficulty: easy
# - Lines: 3
# - Minutes: 3

# %% English
# 1. Modify class `Iris`
# 2. Implement methods:
#    - `Iris.sum()` - returning sum of numeric attributes
#    - `Iris.len()` - returning number of numeric attributes
#    - `Iris.mean()` - returning mean of numeric attributes
# 3. Use property `Iris.numeric_values`
# 4. Run doctests - all must succeed

# %% Polish
# 1. Zmodyfikuj klasę `Iris`
# 2. Zaimplementuj metody:
#    - `Iris.sum()` - zwracającą sumę numerycznych atrybutów
#    - `Iris.len()` - zwracającą liczbę numerycznych atrybutów
#    - `Iris.mean()` - zwracającą średnią numerycznych atrybutów
# 3. Użyj property `Iris.numeric_values`
# 4. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `sum()`
# - `len()`
# - `sum() / len()`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> assert hasattr(Iris, 'mean')
>>> assert hasattr(Iris, 'sum')
>>> assert hasattr(Iris, 'len')

>>> from inspect import isfunction
>>> assert isfunction(Iris.mean)
>>> assert isfunction(Iris.sum)
>>> assert isfunction(Iris.len)

>>> result = Iris(5.1, 3.5, 1.4, 0.2, 'setosa')
>>> result.len()
4
>>> result.sum()
10.2
>>> result.mean()
2.55
"""


class Iris:
    def __init__(self, sl, sw, pl, pw, species):
        self.sepal_length = sl
        self.sepal_width = sw
        self.petal_length = pl
        self.petal_width = pw
        self.species = species

    @property
    def numeric_values(self):
        return tuple(x for x in vars(self).values()
                     if type(x) is float)

    # return sum of numeric attributes
    # type: Callable[[], float]
    def sum(self):
        ...

    # return number of numeric attributes
    # type: Callable[[], int]
    def len(self):
        ...

    # return mean of numeric attributes
    # type: Callable[[], float]
    def mean(self):
        ...