7.4. Metaprogramming Type

7.4.1. SetUp

>>> from pprint import pprint

7.4.2. Recap

  • Type is a class

  • Type takes 1 or 3 arguments

>>> type(1)
<class 'int'>
>>> type(1.2)
<class 'float'>
>>> type(1,2)
Traceback (most recent call last):
TypeError: type() takes 1 or 3 arguments
>>> type((1,2))
<class 'tuple'>

7.4.3. Type Working Modes

  • type() takes 1 or 3 arguments

  • type() has two working modes

  • type(obj) returns a class of an object

  • type() takes str, tuple, dict

  • type(name, bases, attrs) creates a new class

First mode (one argument), will return a class of an object:

>>> type(1)
<class 'int'>

Second mode (three arguments), will create a new class:

>>> type('User', (), {})
<class '__main__.User'>

type() creates classes. It is like a Higgs bozon of Python. While Higgs bozon gives mass to the massless particles, the type gives life to the the non-existent classes.

7.4.4. Class Name

  • First argument is a str with a name for a new class

  • type(name, bases, attrs) creates a new class

The following code:

>>> User = type('User', (), {})

Is equivalent to:

>>> class User:
...     pass

7.4.5. Class Bases

  • Second argument is a tuple with base classes

  • Mind, that one element tuple must have a comma

The following code:

>>> Account = type('Account', (), {})
>>> User = type('User', (Account,), {})

Is equivalent to:

>>> class Account:
...     pass
>>>
>>> class User(Account):
...     pass

7.4.6. Class Variables

  • Third argument is a dict with class variables

  • Mind, that those are class variables

  • Class variables share state between all instances

The following code:

>>> User = type('User', (), {
...     'AGE_MIN': 18,
...     'AGE_MAX': 65,
... })

Is equivalent to:

>>> class User:
...     AGE_MIN = 18
...     AGE_MAX = 65

7.4.7. Class Methods

  • Third argument (dict) can also contains references to functions

  • There is not a big difference, how Python distinguishes between class variables and class methods

  • This is the reason, why we collectively call them class attributes

  • This functions can take self as an argument, but this is not a requirement

  • Function names are not necessary to be the same as in the class

  • Dictionary keys decide, how the method will be called later on

The following code:

>>> def login():
...     print('User login')
...
>>> def logout(self):
...     print('User logout')
...
>>> def myinit(self, firstname, lastname):
...     self.firstname = firstname
...     self.lastname = lastname
...
>>> User = type('User', (), {
...     '__init__': myinit,
...     'login': login,
...     'logout': logout,
... })

Is equivalent to:

>>> class User:
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
...
...     def login():
...         print('User login')
...
...     def logout(self):
...         print('User logout')

7.4.8. All Together

The following code:

>>> def login():
...     print('User login')
...
>>> def logout(self):
...     print('User logout')
...
>>> def myinit(self, firstname, lastname):
...     self.firstname = firstname
...     self.lastname = lastname
...
...
>>> Account = type('Account', (), {})
>>> User = type('User', (Account,), {
...     'AGE_MIN': 18,
...     'AGE_MAX': 65,
...     '__init__': myinit,
...     'login': login,
...     'logout': logout,
... })

Is equivalent to:

>>> class Account:
...     pass
...
>>> class User(Account):
...      AGE_MIN = 18
...      AGE_MAX = 65
...
...      def __init__(self, firstname, lastname):
...          self.firstname = firstname
...          self.lastname = lastname
...
...      def login():
...          print('User login')
...
...      def logout(self):
...          print('User logout')

7.4.9. What is a class?

>>> def login():
...     print('User login')
...
>>> def logout(self):
...     print('User logout')
...
>>> def myinit(self, firstname, lastname):
...     self.firstname = firstname
...     self.lastname = lastname
...
...
>>> Account = type('Account', (), {})
>>> User = type('User', (Account,), {
...     'AGE_MIN': None,
...     'AGE_MAX': None,
...     '__init__': myinit,
...     'login': login,
...     'logout': logout,
... })
>>> hex(id(None))  
0x1062527f0
>>>
>>> hex(id(myinit))  
0x103fb4540
>>>
>>> hex(id(login))  
0x1064e3910
>>>
>>> hex(id(logout))  
0x106082830

The class is effectively a dict with references to memory addresses of other objects: functions, variables, etc.

>>> 
... User = {
...     'AGE_MIN': 0x1062527f0,  # None
...     'AGE_MAX': 0x1062527f0,   # None
...     '__init__': 0x103fb4540,   # myinit
...     'login': 0x1064e3910,      # login
...     'logout': 0x106082830,     # logout
... }

Mind, how similar this is to C language struct:

class User:
    firstname: str
    lastname: str

mark = User()

mark.firstname = "Mark"
mark.lastname = "Watney"

print(mark.firstname)
print(mark.lastname)
struct User {
  char firstname[30];
  char lastname[30];
};

mark = (struct User*) malloc(sizeof(struct User));

mark->firstname = "Mark";
mark->lastname = "Watney";

printf(mark->firstname);
printf(mark->lastname);

This is not a coincidence. Python is written in C language. This makes Python a very powerful language, because it can use all the power of C language. It can be also transpiled to C for example with using cython or mypyc.

7.4.10. Dynamic Class Creation

  • Classes can be created dynamically

  • This is a powerful feature of Python

  • It allows to create classes on the fly

We don't have a class named User:

>>> User()
Traceback (most recent call last):
NameError: name 'User' is not defined

We can create it dynamically together with other classes, by using type(). Registering it in globals() namespace will allow us to use it later on:

>>> for classname in ['User', 'Staff', 'Admin']:
...     globals()[classname] = type(classname, (), {})

Now we have a class named User:

>>> User
<class '__main__.User'>

And we can create an instance of it:

>>> User()  
<__main__.User object at 0x...>

We also have classes Staff and Admin.

7.4.11. Use Case - 1

  • Staticmethod and Classmethod

>>> class User:
...     @staticmethod
...     def login():
...         print('User login')
...
...     @classmethod
...     def logout(cls):
...         print('User logout')

Is equivalent to:

>>> def login():
...     print('User login')
>>>
>>> def logout(cls):
...     print('User logout')
>>>
>>> User = type('User', (), {
...     'login': staticmethod(login),
...     'logout': classmethod(logout),
... })

7.4.12. Use Case - 2

  • Dunder Methods

>>> class User:
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
...
...     def __str__(self):
...         return f'{self.firstname} {self.lastname}'
...
...     def __repr__(self):
...         clsname = self.__class__.__name__
...         firstname = self.firstname
...         lastname = self.lastname
...         return f'{clsname}({firstname=}, {lastname=})'

Is equivalent to:

>>> def myinit(self, firstname, lastname):
...     self.firstname = firstname
...     self.lastname = lastname
>>>
>>> def mystr(self):
...     return f'{self.firstname} {self.lastname}'
>>>
>>> def myrepr(self):
...     clsname = self.__class__.__name__
...     firstname = self.firstname
...     lastname = self.lastname
...     return f'{clsname}({firstname=}, {lastname=})'
>>>
>>>
>>> User = type('User', (), {
...     '__init__': myinit,
...     '__str__': mystr,
...     '__repr__': myrepr,
... })

Usage:

>>> mark = User('Mark', 'Watney')
>>>
>>> print(mark)
Mark Watney
>>>
>>> mark
User(firstname='Mark', lastname='Watney')

7.4.13. Use Case - 1

  • Create a class function login dynamically using lambda expression

>>> User = type('User', (), {
...     'firstname': None,
...     'lastname': None,
...     'login': lambda: print('User login'),
... })
>>> User.login()
User login
>>> result = vars(User)
>>> pprint(result)  
mappingproxy({'__dict__': <attribute '__dict__' of 'User' objects>,
              '__doc__': None,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'User' objects>,
              'firstname': None,
              'lastname': None,
              'login': <function <lambda> at 0x...>})

7.4.14. Use Case - 2

  • Create classes dynamically from data, using species as class names

>>> 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'),
...     (7.0, 3.2, 4.7, 1.4, 'versicolor'),
...     (7.6, 3.0, 6.6, 2.1, 'virginica'),
... ]
>>>
>>>
>>> class Iris:
...     def __init__(self, **kwargs):
...         self.__dict__ = kwargs
...
...     def __repr__(self):
...         clsname = self.__class__.__name__
...         values = tuple(vars(self).values())
...         return f'{clsname}{values}'
>>>
>>>
>>> header, *rows = DATA
>>> result = []
>>>
>>> for *features,species in rows:
...     features = dict(zip(header, features))
...     clsname = species.capitalize()
...     if clsname not in globals():
...         globals()[clsname] = type(clsname, (Iris,), {})
...     cls = globals()[clsname]
...     iris = cls(**features)
...     result.append(iris)
>>>
>>> result  
[Virginica(5.8, 2.7, 5.1, 1.9),
 Setosa(5.1, 3.5, 1.4, 0.2),
 Versicolor(5.7, 2.8, 4.1, 1.3),
 Virginica(6.3, 2.9, 5.6, 1.8),
 Versicolor(6.4, 3.2, 4.5, 1.5),
 Setosa(4.7, 3.2, 1.3, 0.2),
 Versicolor(7.0, 3.2, 4.7, 1.4),
 Virginica(7.6, 3.0, 6.6, 2.1)]
>>>
>>> vars(result[0])  
{'sepal_length': 5.8,
 'sepal_width': 2.7,
 'petal_length': 5.1,
 'petal_width': 1.9}

7.4.15. Use Case - 3

  • Dynamic Classes 2

>>> from dataclasses import dataclass
>>> from itertools import zip_longest
>>>
>>>
>>> DATA = [
...     ('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'),
...     (5.8, 2.7, 'virginica'),
...     (5.1, 3.5, 1.4, 0.2, 'setosa'),
...     (5.7, '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, 'setosa'),
...     (7.0, 3.2, 4.7, 1.4, 'versicolor'),
...     (7.6, 3.0,  'virginica'),
... ]
>>>
>>>
>>> @dataclass(init=False)
... class Iris:
...     def __init__(self, **kwargs):
...         self.__dict__ = kwargs
>>>
>>>
>>> result = []
>>> header, *rows = DATA
>>>
>>> for *features,species in rows:
...     features = dict(zip_longest(header, features, fillvalue=None))
...     clsname = species.capitalize()
...     if clsname not in globals():
...         globals()[clsname] = type(clsname, (Iris,), {})
...     cls = globals()[clsname]
...     iris = cls(**features)
...     result.append(iris)
>>>
>>> result  
[Virginica(5.8, 2.7, None, None, None),
 Setosa(5.1, 3.5, 1.4, 0.2, None),
 Versicolor(5.7, None, None, None, None),
 Virginica(6.3, 2.9, 5.6, 1.8, None),
 Versicolor(6.4, 3.2, 4.5, 1.5, None),
 Setosa(4.7, 3.2, 1.3, None, None),
 Versicolor(7.0, 3.2, 4.7, 1.4, None),
 Virginica(7.6, 3.0, None, None, None)]
>>>
>>> vars(result[0])  
{'sepal_length': 5.8,
 'sepal_width': 2.7,
 'petal_length': None,
 'petal_width': None,
 'species': None}

7.4.16. Use Case - 4

>>> from dataclasses import dataclass
>>>
>>>
>>> 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'),
... ]
>>>
>>>
>>> @dataclass
... class Iris:
...     sepal_length: float
...     sepal_width: float
...     petal_length: float
...     petal_width: float
>>>
>>>
>>> def factory(row):
...     *features, species = row
...     clsname = species.capitalize()
...     cls = type(clsname, (Iris,), {})
...     return cls(*features)
>>>
>>>
>>> result = map(factory, DATA[1:])
>>>
>>> list(result)  
[Virginica(sepal_length=5.8, sepal_width=2.7, petal_length=5.1, petal_width=1.9),
 Setosa(sepal_length=5.1, sepal_width=3.5, petal_length=1.4, petal_width=0.2),
 Versicolor(sepal_length=5.7, sepal_width=2.8, petal_length=4.1, petal_width=1.3),
 Virginica(sepal_length=6.3, sepal_width=2.9, petal_length=5.6, petal_width=1.8),
 Versicolor(sepal_length=6.4, sepal_width=3.2, petal_length=4.5, petal_width=1.5),
 Setosa(sepal_length=4.7, sepal_width=3.2, petal_length=1.3, petal_width=0.2)]

7.4.17. Use Case - 5

>>> from pprint import pprint
>>>
>>>
>>> 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'),
...     (7.0, 3.2, 4.7, 1.4, 'versicolor'),
...     (7.6, 3.0, 6.6, 2.1, 'virginica'),
... ]
>>>
>>>
>>> def myinit(self, sl, sw, pl, pw):
...     self.sl = sl
...     self.sw = sw
...     self.pl = pl
...     self.pw = pw
>>>
>>> def myrepr(self):
...     clsname = self.__class__.__name__
...     values = tuple(vars(self).values())
...     return f'{clsname}{values}'
>>>
>>> iris = type('Iris', (), {'__init__': myinit, '__repr__': myrepr})
>>>
>>> result = [cls(*values)
...           for *values, species in DATA[1:]
...           if (clsname := species.capitalize())
...           and (cls := type(clsname, (iris,), {}))]
>>>
>>>
>>> pprint(result)
[Virginica(5.8, 2.7, 5.1, 1.9),
 Setosa(5.1, 3.5, 1.4, 0.2),
 Versicolor(5.7, 2.8, 4.1, 1.3),
 Virginica(6.3, 2.9, 5.6, 1.8),
 Versicolor(6.4, 3.2, 4.5, 1.5),
 Setosa(4.7, 3.2, 1.3, 0.2),
 Versicolor(7.0, 3.2, 4.7, 1.4),
 Virginica(7.6, 3.0, 6.6, 2.1)]

7.4.18. Use Case - 6

>>> from itertools import zip_longest
>>>
>>>
>>> DATA = [
...     ('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'),
...     (5.8, 2.7, 'virginica'),
...     (5.1, 3.5, 1.4, 0.2, 'setosa'),
...     (5.7, '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, 'setosa'),
...     (7.0, 3.2, 4.7, 1.4, 'versicolor'),
...     (7.6, 3.0,  'virginica'),
... ]
>>>
>>>
>>> Iris = type('Iris', (), {
...     '__init__': lambda self, **kwargs: self.__dict__.update(kwargs),
...     '__repr__': lambda self: f'{self.__class__.__name__}{tuple(vars(self).values())}',
... })
>>>
>>> header, *rows = DATA
>>>
>>> result = [cls(**values)
...           for *features,species in rows
...           if (values := dict(zip_longest(header, features)))
...           and (clsname := species.capitalize())
...           and (cls := type(clsname, (Iris,), {}))]
>>> result  
[Virginica(5.8, 2.7, None, None, None),
 Setosa(5.1, 3.5, 1.4, 0.2, None),
 Versicolor(5.7, None, None, None, None),
 Virginica(6.3, 2.9, 5.6, 1.8, None),
 Versicolor(6.4, 3.2, 4.5, 1.5, None),
 Setosa(4.7, 3.2, 1.3, None, None),
 Versicolor(7.0, 3.2, 4.7, 1.4, None),
 Virginica(7.6, 3.0, None, None, None)]
>>> result[0]
Virginica(5.8, 2.7, None, None, None)
>>> vars(result[0])  
{'sepal_length': 5.8,
 'sepal_width': 2.7,
 'petal_length': None,
 'petal_width': None,
 'species': None}

7.4.19. Use Case - 7

SetUp:

>>> del Iris
>>> del Setosa
>>> del Virginica
>>> del Versicolor
>>> del cls
>>> del values
>>> del species
>>> del iris
>>> del result

Code:

>>> 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, 'arctica'),
...     (7.0, 3.2, 4.7, 1.4, 'versicolor'),
...     (7.6, 3.0, 6.6, 2.1, 'virginica'),
... ]
>>>
>>>
>>> globals()['Iris'] = type('Iris', (), {
...     '__init__': lambda self, *args: setattr(self, 'features', tuple(args)),
...     '__repr__': lambda self: f'{self.__class__.__name__}{self.features}',
... })
>>>
>>>
>>> def iris(row):
...     *values, species = row
...     clsname = species.capitalize()
...     if clsname not in globals():
...         cls = type(clsname, (globals()['Iris'],), {})
...     return cls(*values)
>>>
>>>
>>> result = map(iris, DATA[1:])
>>>
>>> list(result)  
[Virginica(5.8, 2.7, 5.1, 1.9),
 Setosa(5.1, 3.5, 1.4, 0.2),
 Versicolor(5.7, 2.8, 4.1, 1.3),
 Virginica(6.3, 2.9, 5.6, 1.8),
 Versicolor(6.4, 3.2, 4.5, 1.5),
 Arctica(4.7, 3.2, 1.3, 0.2),
 Versicolor(7.0, 3.2, 4.7, 1.4),
 Virginica(7.6, 3.0, 6.6, 2.1)]

7.4.20. Use Case - 8

>>> 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, 'arctica'),
...     (7.0, 3.2, 4.7, 1.4, 'versicolor'),
...     (7.6, 3.0, 6.6, 2.1, 'virginica'),
... ]
...
>>> header, *rows = DATA
...
>>> globals()['Iris'] = type('Iris', (), {
...     '__init__': lambda self, **kwargs: self.__dict__.update(kwargs),
...     '__str__': lambda self: f'{self.__class__.__name__}{tuple(vars(self).values())}',
... })
...
>>> def factory(row):
...     *values, species = row
...     clsname = species.capitalize()
...     if clsname not in globals():
...         globals()[clsname] = type(clsname, (globals()['Iris'],), {})
...     cls = globals()[clsname]
...     kwargs = dict(zip(header, values))
...     return cls(**kwargs)
...
>>> for iris in map(factory, rows):
...     print(iris)
...
Virginica(5.8, 2.7, 5.1, 1.9)
Setosa(5.1, 3.5, 1.4, 0.2)
Versicolor(5.7, 2.8, 4.1, 1.3)
Virginica(6.3, 2.9, 5.6, 1.8)
Versicolor(6.4, 3.2, 4.5, 1.5)
Arctica(4.7, 3.2, 1.3, 0.2)
Versicolor(7.0, 3.2, 4.7, 1.4)
Virginica(7.6, 3.0, 6.6, 2.1)

7.4.21. Use Case - 9

>>> def user(*args, **kwargs):
...     clsname = 'User'
...     if clsname not in globals():
...         globals()[clsname] = type(clsname, (), {
...             '__init__': lambda self, *args, **kwargs: setattr(self, '__dict__', kwargs),
...             '__str__': lambda self: f'{self.firstname} {self.lastname}',
...             '__repr__': lambda self: f'{self.__class__.__name__}({self.firstname}, {self.lastname}, {self.age})',
...         })
...     cls = globals()[clsname]
...     return cls(*args, **kwargs)
>>>
>>>
>>> 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},
... ]
>>>
>>> data = DATA[0]
>>> user(**data)
User(Mark, Watney, 41)

7.4.22. 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 ClassFactory Iris
# - Difficulty: medium
# - Lines: 8
# - Minutes: 8

# %% English
# 1. Define `result: list[Iris]`
# 2. Iterate over `DATA` rows (skip header - first row)
# 3. Separate `values` from `species` in each row
# 4. Append to `result`:
#    - if `species` is "setosa" append an instance of a class `Setosa`
#    - if `species` is "versicolor" append an instance of a class `Versicolor`
#    - if `species` is "virginica" append an instance of a class `Virginica`
# 5. Initialize instances with `values` using `*args` notation
# 6. Run doctests - all must succeed

# %% Polish
# 1. Zdefiniuj `result: list[Iris]`
# 2. Iteruj po wierszach w `DATA` (pomiń nagłówek - pierwszy wiersz)
# 3. Odseparuj `values` od `species` w każdym wierszu
# 4. Dodaj do `result`:
#    - jeżeli `species` jest "setosa" to dodaj instancję klasy `Setosa`
#    - jeżeli `species` jest "versicolor" to dodaj instancję klasy `Versicolor`
#    - jeżeli `species` jest "virginica" to dodaj instancję klasy `Virginica`
# 5. Instancje inicjalizuj danymi z `values` używając notacji `*args`
# 6. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `globals()[classname]`

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

>>> list(result)  # doctest: +NORMALIZE_WHITESPACE
[Virginica(5.8, 2.7, 5.1, 1.9),
 Setosa(5.1, 3.5, 1.4, 0.2),
 Versicolor(5.7, 2.8, 4.1, 1.3),
 Virginica(6.3, 2.9, 5.6, 1.8),
 Versicolor(6.4, 3.2, 4.5, 1.5),
 Setosa(4.7, 3.2, 1.3, 0.2)]
"""

from dataclasses import dataclass


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'),
]

header, *rows = DATA


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

    def __repr__(self):
        name = self.__class__.__name__
        args = tuple(self.__dict__.values())
        return f'{name}{args}'


# Append to `result`:
# Use type() to create classes dynamically
# - if `species` is "setosa" append an instance of a class `Setosa`
# - if `species` is "versicolor" append an instance of a class `Versicolor`
# - if `species` is "virginica" append an instance of a class `Virginica`
# type: list[Iris]
result = ...