5.1. OOP Slots

  • Slots are faster and save memory

  • Slots prevent from adding new attributes

  • Slotted classes don't have __dict__ and __weakref__

  • Slotted classes have __slots__ and slotted attributes

  • Slots do not affect methods or __init__()

  • Slots do not inherit, unless subclass is also slotted

>>> class User:
...     __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> mark = User()
>>>
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>>
>>> mark.age = 41
Traceback (most recent call last):
AttributeError: 'User' object has no attribute 'age' and no __dict__ for setting new attributes

5.1.1. Definition

  • Add __slots__ to class definition

  • It could be list[str] or tuple[str] or dict[str,str]

  • Format of dict[str,str] is used for docstrings of slotted fields

Slots as a tuple:

>>> class User:
...     __slots__ = ('firstname', 'lastname')

Slots as a list:

>>> class User:
...     __slots__ = ['firstname', 'lastname']

Slots as a dict:

>>> class User:
...     __slots__ = {
...         'firstname': 'Docstring for firstname attribute',
...         'lastname': 'Docstring for lastname attribute',
...     }

If a dictionary is used to assign __slots__, the dictionary keys will be used as the slot names. The values of the dictionary can be used to provide per-attribute docstrings that will be recognised by inspect.getdoc() and displayed in the output of help() [1]:

Mind, that single element tuple must have a comma at the end:

>>> class User:
...     __slots__ = ('fullname',)

5.1.2. Under the Hood

  • Unslotted classes will have __dict__ and __weakref__

  • Slots will not have __dict__ and __weakref__

  • Slots will have __slots__ and slotted attributes

__slots__ are implemented at the class level by creating descriptors for each variable name. As a result, class attributes cannot be used to set default values for instance variables defined by __slots__; otherwise, the class attribute would overwrite the descriptor assignment [1].

Unslotted class:

>>> class User:
...     pass
>>>
>>>
>>> vars(User)  
mappingproxy({'__module__': '__main__',
              '__firstlineno__': 1,
              '__static_attributes__': (),
              '__dict__': <attribute '__dict__' of 'User' objects>,
              '__weakref__': <attribute '__weakref__' of 'User' objects>,
              '__doc__': None})

Slotted class:

>>> class User:
...     __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> vars(User)  
mappingproxy({'__module__': '__main__',
              '__firstlineno__': 1,
              '__slots__': ('firstname', 'lastname'),
              '__static_attributes__': (),
              'firstname': <member 'firstname' of 'User' objects>,
              'lastname': <member 'lastname' of 'User' objects>,
              '__doc__': None})

5.1.3. Setattr

  • In unslotted classes you can add attributes dynamically

  • Slots disables adding attributes dynamically

Unslotted:

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

Slotted:

>>> class User:
...     __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>>
>>> mark.age = 41
Traceback (most recent call last):
AttributeError: 'User' object has no attribute 'age' and no __dict__ for setting new attributes

Setting an attribute not listed in __slots__ will raise an error:

5.1.4. Getattr

  • You can access slotted attributes as normal

>>> class User:
...     __slots__ = ('firstname', 'lastname')
>>>
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>>
>>> print(mark.firstname)
Mark

5.1.5. Methods

  • Slots do not affect methods

  • You can define methods as usual

  • You can access slotted attributes inside methods

>>> class User:
...     __slots__ = ('firstname', 'lastname')
...
...     def fullname(self):
...         return f'{self.firstname} {self.lastname}'
>>>
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>>
>>> mark.fullname()
'Mark Watney'

5.1.6. Init

  • Slots do not affect __init__()

  • You can define it as usual

  • You can set slotted attributes inside __init__()

  • You cannot assign to not slotted attribute

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

Even inside of __init__ function you cannot assign to not slotted attribute:

>>> class User:
...     __slots__ = ('firstname', 'lastname')
...
...     def __init__(self, firstname, lastname, age):
...         self.firstname = firstname
...         self.lastname = lastname
...         self.age = age
>>>
>>>
>>> mark = User('Mark', 'Watney', age=41)
Traceback (most recent call last):
AttributeError: 'User' object has no attribute 'age' and no __dict__ for setting new attributes

5.1.7. Vars and __dict__

  • Slots will prevent from creating __dict__

  • Builtin function vars() will not work on slots

  • vars() requires __dict__

Without a __dict__ variable, instances cannot be assigned new variables not listed in the __slots__ definition. Attempts to assign to an unlisted variable name raises AttributeError. If dynamic assignment of new variables is desired, then add '__dict__' to the sequence of strings in the __slots__ declaration [1].

>>> class User:
...     __slots__ = ('firstname', 'lastname')
...
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>>
>>> mark.__dict__
Traceback (most recent call last):
AttributeError: 'User' object has no attribute '__dict__'. Did you mean: '__dir__'?
>>>
>>> vars(mark)
Traceback (most recent call last):
TypeError: vars() argument must have __dict__ attribute

5.1.8. Recreating Vars Behavior

  • To get values iterate over self.__slots__ and use getattr(self, x)

  • {x:getattr(mark,x) for x in mark.__slots__}

>>> class User:
...     __slots__ = ('firstname', 'lastname')
...
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
>>>
>>>
>>> mark = User('Mark', 'Watney')
>>>
>>> {x:getattr(mark,x) for x in mark.__slots__}
{'firstname': 'Mark', 'lastname': 'Watney'}

5.1.9. Fallback

  • User.__slots__ = ('firstname', 'lastname', '__dict__')

>>> class User:
...     __slots__ = ('firstname', 'lastname', '__dict__')
>>>
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>> mark.age = 41
>>> mark.firstname
'Mark'
>>>
>>> mark.lastname
'Watney'
>>>
>>> mark.age
41
>>> vars(mark)
{'age': 41}
>>>
>>> mark.__dict__
{'age': 41}

5.1.10. Inheritance Problem

  • Slots do not inherit, unless they are specified in subclass

  • Slots are added on inheritance

  • If class does not specify slots, the __dict__ will be added

The action of a __slots__ declaration is not limited to the class where it is defined. __slots__ declared in parents are available in child classes. However, child subclasses will get a __dict__ and __weakref__ unless they also define __slots__ (which should only contain names of any additional slots) [1].

Multiple inheritance with multiple slotted parent classes can be used, but only one parent is allowed to have attributes created by slots (the other bases must have empty slot layouts) - violations raise TypeError [1].

If a class defines a slot also defined in a base class, the instance variable defined by the base class slot is inaccessible (except by retrieving its descriptor directly from the base class). This renders the meaning of the program undefined. In the future, a check may be added to prevent this.

Definition:

>>> class User:
...     __slots__ = ('firstname', 'lastname')
>>>
>>> class Admin(User):
...     pass

Result

>>> vars(User)  
mappingproxy({'__module__': '__main__',
              '__firstlineno__': 1,
              '__slots__': ('firstname', 'lastname'),
              '__static_attributes__': (),
              'firstname': <member 'firstname' of 'User' objects>,
              'lastname': <member 'lastname' of 'User' objects>,
              '__doc__': None})
>>> vars(Admin)  
mappingproxy({'__module__': '__main__',
              '__firstlineno__': 1,
              '__static_attributes__': (),
              '__dict__': <attribute '__dict__' of 'Admin' objects>,
              '__weakref__': <attribute '__weakref__' of 'Admin' objects>,
              '__doc__': None})

5.1.11. Inheritance Solution

Definition:

>>> class User:
...     __slots__ = ('firstname', 'lastname')
>>>
>>> class Admin(User):
...     __slots__ = ()

Result:

>>> vars(User)  
mappingproxy({'__module__': '__main__',
              '__firstlineno__': 1,
              '__slots__': ('firstname', 'lastname'),
              '__static_attributes__': (),
              'firstname': <member 'firstname' of 'User' objects>,
              'lastname': <member 'lastname' of 'User' objects>,
              '__doc__': None})
>>> vars(Admin)  
mappingproxy({'__module__': '__main__',
              '__firstlineno__': 1,
              '__slots__': (),
              '__static_attributes__': (),
              '__doc__': None})

5.1.12. Dataclasses

  • Since Python 3.10

  • Parameter @dataclass(slots=True) will add slots to the class

Definition:

>>> from dataclasses import dataclass
>>>
>>> @dataclass(slots=True)
... class User:
...     firstname: str
...     lastname: str

Inheritance:

>>> from dataclasses import dataclass
>>>
>>> @dataclass(slots=True)
... class User:
...     firstname: str
...     lastname: str
>>>
>>>
>>> @dataclass(slots=True)
... class Admin(User):
...     pass

5.1.13. Case Study

>>> from sys import getsizeof
>>> from itertools import chain
>>> from collections import deque
>>>
>>>
>>>
>>> def deepsizeof(o, handlers={}):
...     """
...     Returns the approximate memory footprint an object and all of its contents.
...
...     Automatically finds the contents of the following builtin containers and
...     their subclasses: tuple, list, deque, dict, set and frozenset
...     """
...     dict_handler = lambda d: chain.from_iterable(d.items())
...     all_handlers = {tuple: iter,
...                     list: iter,
...                     deque: iter,
...                     dict: dict_handler,
...                     set: iter,
...                     frozenset: iter}
...     all_handlers.update(handlers)     # user handlers take precedence
...     seen = set()                      # track which object id's have already been seen
...     default_size = getsizeof(0)       # estimate sizeof object without __sizeof__
...
...     def sizeof(o):
...         if id(o) in seen:       # do not double count the same object
...             return 0
...         seen.add(id(o))
...         s = getsizeof(o, default_size)
...
...         for typ, handler in all_handlers.items():
...             if isinstance(o, typ):
...                 s += sum(map(sizeof, handler(o)))
...                 break
...         else:
...             if not hasattr(o.__class__, '__slots__'):
...                 if hasattr(o, '__dict__'):
...                     # no __slots__ *usually* means a
...                     # __dict__, but some special builtin classes (such
...                     # as `type(None)`) have neither
...                     # else, `o` has no attributes at all, so sys.getsizeof()
...                     # actually returned the correct value
...                     s += sizeof(o.__dict__)
...             else:
...                 s += sum(
...                     sizeof(getattr(o, x))
...                            for x in o.__class__.__slots__
...                            if hasattr(o, x))
...         return s
...     return sizeof(o)

Test:

>>> class User:
...    __slots__ = ('firstname', 'lastname')
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>>
>>> deepsizeof(mark)
140
>>> class User:
...     pass
>>>
>>>
>>> mark = User()
>>> mark.firstname = 'Mark'
>>> mark.lastname = 'Watney'
>>>
>>> deepsizeof(mark)
535

5.1.14. Use Case - 1

>>> class User:
...     __slots__ = ('firstname', 'lastname', 'age')
...
...     def __init__(self, firstname, lastname, age):
...         self.firstname = firstname
...         self.lastname = lastname
...         self.age = age
...
...     def __repr__(self):
...         clsname = self.__class__.__name__
...         firstname = self.firstname
...         lastname = self.lastname
...         age = self.age
...         return f'{clsname}({firstname=}, {lastname=}, {age=})'
>>>
>>>
>>> DATA = [
...     ('Mark', 'Watney', 41),
...     ('Melissa', 'Lewis', 40),
...     ('Rick', 'Martinez', 39),
...     ('Alex', 'Vogel', 40),
...     ('Chris', 'Beck', 36),
...     ('Beth', 'Johanssen', 29),
... ]
>>>
>>> result = [User(*row) for row in DATA]

Result:

>>> result  
[User(firstname='Mark', lastname='Watney', age=41),
 User(firstname='Melissa', lastname='Lewis', age=40),
 User(firstname='Rick', lastname='Martinez', age=39),
 User(firstname='Alex', lastname='Vogel', age=40),
 User(firstname='Chris', lastname='Beck', age=36),
 User(firstname='Beth', lastname='Johanssen', age=29)]

5.1.15. Use Case - 3

>>> class User:
...     __slots__ = ('firstname', 'lastname', 'age')
...
...     def __init__(self, firstname, lastname, age):
...         self.firstname = firstname
...         self.lastname = lastname
...         self.age = age
...
...     def __repr__(self):
...         clsname = self.__class__.__name__
...         firstname = self.firstname
...         lastname = self.lastname
...         age = self.age
...         return f'{clsname}({firstname=}, {lastname=}, {age=})'
>>>
>>>
>>> DATA = [
...     {'firstname': 'Mark', 'lastname': 'Watney', 'age': 41},
...     {'firstname': 'Melissa', 'lastname': 'Lewis', 'age': 40},
...     {'firstname': 'Rick', 'lastname': 'Martinez', 'age': 39},
...     {'firstname': 'Alex', 'lastname': 'Vogel', 'age': 40},
...     {'firstname': 'Chris', 'lastname': 'Beck', 'age': 36},
...     {'firstname': 'Beth', 'lastname': 'Johanssen', 'age': 29},
... ]
>>>
>>> result = [User(**row) for row in DATA]

Result:

>>> result  
[User(firstname='Mark', lastname='Watney', age=41),
 User(firstname='Melissa', lastname='Lewis', age=40),
 User(firstname='Rick', lastname='Martinez', age=39),
 User(firstname='Alex', lastname='Vogel', age=40),
 User(firstname='Chris', lastname='Beck', age=36),
 User(firstname='Beth', lastname='Johanssen', age=29)]

5.1.16. Use Case - 3

>>> from dataclasses import dataclass
>>>
>>>
>>> @dataclass(slots=True)
... class User:
...     firstname: str
...     lastname: str
...     age: int
>>>
>>>
>>> DATA = [
...     ('Mark', 'Watney', 41),
...     ('Melissa', 'Lewis', 40),
...     ('Rick', 'Martinez', 39),
...     ('Alex', 'Vogel', 40),
...     ('Chris', 'Beck', 36),
...     ('Beth', 'Johanssen', 29),
... ]
>>>
>>> result = [User(*row) for row in DATA]

Result:

>>> result  
[User(firstname='Mark', lastname='Watney', age=41),
 User(firstname='Melissa', lastname='Lewis', age=40),
 User(firstname='Rick', lastname='Martinez', age=39),
 User(firstname='Alex', lastname='Vogel', age=40),
 User(firstname='Chris', lastname='Beck', age=36),
 User(firstname='Beth', lastname='Johanssen', age=29)]

5.1.17. Recap

  • Slots are faster and save memory

  • Slots prevent from adding new attributes

  • Slotted classes don't have __dict__ and`` __weakref__``

  • Slotted classes have`` __slots__`` and slotted attributes

  • Slots do not affect methods or __init__()

  • Slots do not inherit, unless subclass is also slotted

5.1.18. Further Reading

5.1.19. References

5.1.20. 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 AttributeSlots Dataclass
# - Difficulty: easy
# - Lines: 4
# - Minutes: 3

# %% English
# 1. Define dataclass `User` with slots:
#    - `firstname: str`
#    - `lastname: str`
# 2. Run doctests - all must succeed

# %% Polish
# 1. Zdefiniuj dataklasę `User` ze slotami:
#    - `firstname: str`
#    - `lastname: str`
# 2. Uruchom doctesty - wszystkie muszą się powieść

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

>>> from dataclasses import is_dataclass

>>> assert hasattr(User, '__slots__')
>>> assert 'firstname' in User.__slots__
>>> assert 'lastname' in User.__slots__

>>> assert User is not Ellipsis, \
'Assign result to variable: `User`'
>>> assert type(User) is type, \
'Result must be a type'
>>> assert is_dataclass(User), \
'Class User has to be dataclass'

>>> result = User(firstname='Mark', lastname='Watney')
>>> assert not hasattr(result, '__dict__')
>>> assert not hasattr(result, '__weakref__')
"""
from dataclasses import dataclass


# Define dataclass `User` with slots:
# - `firstname: str`
# - `lastname: str`
# type: type[User]
@dataclass
class User:
    ...


# %% 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 AttributeSlots Define
# - Difficulty: easy
# - Lines: 2
# - Minutes: 2

# %% English
# 1. Define class `User` with slots:
#    - `firstname: str`
#    - `lastname: str`
# 2. Do not define `__init__()` method
# 3. Do not use dataclass
# 4. Run doctests - all must succeed

# %% Polish
# 1. Zdefiniuj klasę `User` ze slotami:
#    - `firstname: str`
#    - `lastname: str`
# 2. Nie definiuj metody `__init__()`
# 3. Nie używaj dataclass
# 4. Uruchom doctesty - wszystkie muszą się powieść

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

>>> from dataclasses import is_dataclass

>>> assert User is not Ellipsis, \
'Assign result to variable: `User`'
>>> assert type(User) is type, \
'Result must be a type'
>>> assert not is_dataclass(User), \
'Class User cannot be dataclass'

>>> assert hasattr(User, '__slots__')
>>> assert 'firstname' in User.__slots__
>>> assert 'lastname' in User.__slots__

>>> result = User()
>>> assert not hasattr(result, '__dict__')
>>> assert not hasattr(result, '__weakref__')
"""

# Define class `User` with slots:
# - `firstname: str`
# - `lastname: str`
# Do not define `__init__()` method
# Do not use dataclass
# type: type[User]
class User:
    ...


# %% 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 AttributeSlots Init
# - Difficulty: easy
# - Lines: 3
# - Minutes: 2

# %% English
# 1. Define class `User` with slots:
#    - `firstname: str`
#    - `lastname: str`
# 2. Define `__init__()` method
# 3. Do not use dataclass
# 4. Run doctests - all must succeed

# %% Polish
# 1. Zdefiniuj klasę `User` z slotami:
#    - `firstname: str`
#    - `lastname: str`
# 2. Zdefiniuj metodę `__init__()`
# 3. Nie używaj dataclass
# 4. Uruchom doctesty - wszystkie muszą się powieść

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

>>> from dataclasses import is_dataclass

>>> assert hasattr(User, '__slots__')
>>> assert 'firstname' in User.__slots__
>>> assert 'lastname' in User.__slots__

>>> assert User is not Ellipsis, \
'Assign result to variable: `User`'
>>> assert type(User) is type, \
'Result must be a type'
>>> assert not is_dataclass(User), \
'Class User cannot be dataclass'

>>> result = User(firstname='Mark', lastname='Watney')
>>> assert not hasattr(result, '__dict__')
>>> assert not hasattr(result, '__weakref__')
"""

# Define class `User` with slots:
# - `firstname: str`
# - `lastname: str`
# Define `__init__()` method
# type: type[User]
class User:
    __slots__ = ('firstname', 'lastname')


# %% 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 AttributeSlots Init
# - Difficulty: easy
# - Lines: 2
# - Minutes: 3

# %% English
# 1. Define function `dump(obj) -> dict` accepting instance with slots
# 2. Function should return similar output to `vars()`, i.e.:
#    {'firstname':'mwatney', 'lastname':'Ares3'}
# 3. Run doctests - all must succeed

# %% Polish
# 1. Zdefiniuj funkcję `dump(obj) -> dict` przyjmującą instancję ze slotami
# 2. Funkcja powinna zwracać podobny wynik do `vars()`, np:
#    {'firstname':'mwatney', 'lastname':'Ares3'}
# 3. Uruchom doctesty - wszystkie muszą się powieść

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

>>> from dataclasses import is_dataclass

>>> class User:
...     __slots__ = ('firstname', 'lastname')
...
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
>>>
>>>
>>> mark = User(firstname='Mark', lastname='Watney')
>>> result = dump(mark)

>>> assert result is not Ellipsis, \
'Assign result to variable: `result`'
>>> assert type(result) is dict, \
'Result must be a type'
>>> assert len(result) == 2, \
'Result length must be 2'
>>> assert all(type(x) is str for x in result.keys()), \
'All keys in result must be a str'
>>> assert all(type(x) is str for x in result.values()), \
'All values in result must be a str'

>>> result
{'firstname': 'Mark', 'lastname': 'Watney'}
"""

# Define function `dump(obj) -> dict` accepting instance with slots
# Function should return similar output to `vars()`
# type: Callable[[object], dict]
def dump(obj) -> dict:
    ...


# %% 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 AttributeSlots Repr
# - Difficulty: medium
# - Lines: 4
# - Minutes: 5

# %% English
# 1. Define method `__repr__` which prints class name and all values
#    positionally, ie. `Iris(5.8, 2.7, 5.1, 1.9, 'virginica')`
# 2. Run doctests - all must succeed

# %% Polish
# 1. Zdefiniuj metodę `__repr__` wypisującą nazwę klasy i wszystkie
#    wartości atrybutów pozycyjnie, np. `Iris(5.8, 2.7, 5.1, 1.9, 'virginica')`
# 2. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `self.__class__.__name__`
# - `tuple()`
# - `dict.values()`

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

>>> result = [Iris(*row) for row in DATA[1:]]
>>> result  # doctest: +NORMALIZE_WHITESPACE
[Iris(5.8, 2.7, 5.1, 1.9, 'virginica'),
 Iris(5.1, 3.5, 1.4, 0.2, 'setosa'),
 Iris(5.7, 2.8, 4.1, 1.3, 'versicolor'),
 Iris(6.3, 2.9, 5.6, 1.8, 'virginica'),
 Iris(6.4, 3.2, 4.5, 1.5, 'versicolor'),
 Iris(4.7, 3.2, 1.3, 0.2, 'setosa')]

>>> iris = result[0]
>>> iris
Iris(5.8, 2.7, 5.1, 1.9, 'virginica')

>>> iris.__slots__
('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species')

>>> [getattr(iris, x) for x in iris.__slots__]
[5.8, 2.7, 5.1, 1.9, 'virginica']

>>> {x: getattr(iris, x)
...  for x in iris.__slots__}  # doctest: +NORMALIZE_WHITESPACE
{'sepal_length': 5.8,
 'sepal_width': 2.7,
 'petal_length': 5.1,
 'petal_width': 1.9,
 'species': 'virginica'}

>>> iris.__dict__
Traceback (most recent call last):
AttributeError: 'Iris' object has no attribute '__dict__'. Did you mean: '__dir__'?

>>> values = tuple(getattr(iris, x) for x in iris.__slots__)
>>> print(f'Iris{values}')
Iris(5.8, 2.7, 5.1, 1.9, 'virginica')
"""

DATA = [
    ('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'),
    (5.8, 2.7, 5.1, 1.9, 'virginica'),
    (5.1, 3.5, 1.4, 0.2, 'setosa'),
    (5.7, 2.8, 4.1, 1.3, 'versicolor'),
    (6.3, 2.9, 5.6, 1.8, 'virginica'),
    (6.4, 3.2, 4.5, 1.5, 'versicolor'),
    (4.7, 3.2, 1.3, 0.2, 'setosa'),
]


class Iris:
    __slots__ = ('sepal_length', 'sepal_width', 'petal_length',
                 'petal_width', 'species')

    def __init__(self, sepal_length, sepal_width,
                 petal_length, petal_width, species):
        self.sepal_length = sepal_length
        self.sepal_width = sepal_width
        self.petal_length = petal_length
        self.petal_width = petal_width
        self.species = species

    def _dump(self) -> dict:
        return {x:getattr(self,x) for x in self.__slots__}

# Define method `__repr__` which prints class name and all values
# positionally, ie. `Iris(5.8, 2.7, 5.1, 1.9, 'virginica')`
# type: Callable[[Self], str]

    def __repr__(self):
        ...