11. Classes

Classes are the way to model your code after real-world objects. The act of coding to model real-world objects is called Object Oriented Programming or OOP. Classes define two things about objects: their properties and actions. Properties are simply traits, characteristics or attributes of an object. Actions are what the object can do. As such, properties are variables attached to a class and actions are functions attached to a class. When you start defining an object with a class (or start writing a class definition), always keep in mind the properties and actions that are important to that object and make a sketch or outline. Let’s do some examples.

If you are trying to model a person, what are some properties of a person and what can a person do? A person has a first and last name, as well as an age. A person can also eat, sleep or talk.

  • Person
    • properties
      • first name

      • last name

      • age

    • actions
      • eat

      • sleep

      • talk

If you are trying to model a square, what are some properties and actions of a square? A square has a side and can compute its own area and perimeter.

  • Triangle
    • properties
      • side

    • actions
      • compute area

      • compute perimeter

What about modeling a car? A car has a make, model, year, doors and gas efficiency rating. A car can start up, speed up, slow down or stop.

  • Car
    • properties
      • make

      • model

      • year

      • number of doors

      • miles per gallon

    • actions
      • start up

      • speed

      • slow

      • stop

Note

It is very helpful to name properties to be like nouns and to name functions (actions) to be like verbs. Names of properties should be nouny and names of functions should be verby.

11.1. Basic

The best way to understand how to define an object is by looking at the simplest examples possible. Below, we are starting to define classes for a Person, Triangle and Car. Since we just want to view the simplest possible way to define classes without providing actual logic, we use the pass statement as a placeholder for future code.

1class Person(object):
2   pass
3
4class Triangle(object):
5   pass
6
7class Car(object):
8   pass

Note

In Python 2, classes could be defined as follows.

class Person:
   pass

This way of declaring a class is called old-style class definition. The new-style class definition is to make all class inherit from object explicitly as follows.

class Person(object):
   pass

In Python 3, there are only new-style classes, and whether you explicitly inherit from object or not, all classes in Python 3 are new-style. We encourage coders to explicitly inherit from object regardless of using Python 2 or 3. In Python 2, if you use old-style class definition, then the class (by way of the .__class__ property) and type (by way of calling type()) are not aligned. There are many benefits of using new-style classes.

11.2. Constructor

Let’s just focus on the class definition of Car. Below, only the properties of a car are defined (make, model and year), and no actions are being modeled and defined, yet. However, there is one action (or method) that we have defined with the def keyword called __init__(). The function __init__() is called the constructor of the class; when we create an instance of a Car, this constructor method is called to create the instance and its state. The constructor dictates the essential properties that we must supply when we create an instance of a class. Notice that we have self as a part of the arguments to the constructor? What is self? What is self doing there in the signature of the constructor? self refers to the instance of Car that we are trying to create, which is passed along with the properties.

1class Car(object):
2   def __init__(self, make, model, year):
3      pass

Note

Sometimes, the constructor is referred to as the ctor.

How do we use self? We use self to associate the instance with the properties. Note how we use dot . notation to say the instance’s make self.make is assigned to the make passed into the constructor, and the same for model and year as well.

1class Car(object):
2   def __init__(self, make, model, year):
3      self.make = make
4      self.model = model
5      self.year = year

Note

The constructor __init__() has two underscores _ before and after the name init. Why? By convention, Python has specified that magic methods have double underscores before and after the method name. These magic methods are sometimes called dunders for double underscores. Dunders are magic methods in the sense that if they are defined, then they may be used with functions and operators with the intended, natural result. For example, another dunder is __repr__() which should return a string representation of an instance of a class. When issuing print() on an instance of a class, __repr__() will be called to get the string representation.

11.3. Instantiation

Now that we have class definition for Car, how do we create an instance of Car? Remember, according to the constructor, we need to always pass in the make, model and year. When create an instance, we simply refer to the name of the class with parentheses, and inside the parentheses, supply the required properties. Note that self is not supplied, Python will do that part.

1class Car(object):
2   def __init__(self, make, model, year):
3      self.make = make
4      self.model = model
5      self.year = year
6
7# instantiation: create a Car instance
8car = Car('Honda', 'Accord', 2019)

After we have an instance of Car, we can access the properties (and methods) using dot . notation.

 1class Car(object):
 2   def __init__(self, make, model, year):
 3      self.make = make
 4      self.model = model
 5      self.year = year
 6
 7# instantiation: create a Car instance
 8car = Car('Honda', 'Accord', 2019)
 9
10# access the properties
11print(car.make)
12print(car.model)
13print(car.year)
14print(f'{car.make} {car.model} {car.year}')

11.4. Class methods

As stated before, classes has properties and actions. Actions defined for a class is no different than the general way of defining a function. However, the self argument is always supplied to class functions or methods.

 1class Car(object):
 2   def __init__(self, make, model, year):
 3      self.make = make
 4      self.model = model
 5      self.year = year
 6
 7   def start_up(self):
 8      print('starting up')
 9
10   def speed(self):
11      print('increasing speed')
12
13   def slow_down(self):
14      print('slowing down')
15
16   def stop(self):
17      print('stopping')
18
19# instantiation: create a Car instance
20car = Car('Honda', 'Accord', 2019)
21
22# access, call or invoke the methods of an instance of Car
23car.start_up()
24car.speed()
25car.slow_down()
26car.stop()

11.4.1. Getters and setters

If you come from other languages like Java or C#, it is typical to prevent direct read and write acces to properties. You may generate getters and setters to read and write to properties, correspondingly.

 1class Car(object):
 2    def __init__(self, make, model, year):
 3        self.make = make
 4        self.model = model
 5        self.year = year
 6
 7    def get_make(self):
 8        return self.make
 9
10    def get_model(self):
11        return self.model
12
13    def get_year(self):
14        return self.year
15
16    def set_make(self, make):
17        self.make = make
18
19    def set_model(self, model):
20        self.model = model
21
22    def set_year(self, year):
23        self.year = year
24
25
26car = Car('Honda', 'Accord', 2019)
27
28car.set_make('Toyota')
29car.set_model('Supra')
30car.set_year(2019)
31
32print(f'{car.get_make()} {car.get_model()} {car.get_year()}')

Note

In general, getters are called accessors and setters are called mutators. Typically, in Python, it is not common to see getters and setters being defined for classes. It seems to be that Pythonistas want code to be concise.

11.4.2. Access specifiers

In OOP, you may desire to specify who can reference your class properties or invoke your class functions. Typically, there are three levels of access.

  • public: anyone can have access

  • protected: only sub-classes can have access

  • private: only the class itself can have access

The convention to signal access specifiers is with underscores _.

  • public: names of properties or functions of a class without any underscore are public

  • protected: names of properties or functions of a class with one underscore as the prefix are protected

  • private: names of properties or functions of a class with two underscores as the prefix are private

Below, we have defined __check_year() to be private, and users cannot not invoke this method directly. The set_year() method calls __check_year() internally when the year is being mutated.

 1class Car(object):
 2    def __init__(self, make, model, year):
 3        self.make = make
 4        self.model = model
 5        self.year = self.__check_year(year)
 6
 7    def __check_year(self, year):
 8        return 2019 if year < 1950 else year
 9
10    def get_make(self):
11        return self.make
12
13    def get_model(self):
14        return self.model
15
16    def get_year(self):
17        return self.year
18
19    def set_make(self, make):
20        self.make = make
21
22    def set_model(self, model):
23        self.model = model
24
25    def set_year(self, year):
26        self.year = self.__check_year(year)
27
28
29car = Car('Honda', 'Accord', 2019)
30
31car.set_make('Toyota')
32car.set_model('Supra')
33car.set_year(1800)
34
35print(f'{car.get_make()} {car.get_model()} {car.get_year()}')

Here’s another example of modeling a student. Notice that the first and last names are stored internally as private properties (two double underscores). Thus, we cannot access those properties directly. We have to access through the decorated methods (those methods are public).

 1class Student(object):
 2   def __init__(self, fname, lname):
 3      self.__fname = fname
 4      self.__lname = lname
 5
 6   @property
 7   def fname(self):
 8      return self.__fname
 9
10   @property
11   def lname(self):
12      return self.__lname
13
14student1 = Student('John', 'Doe')
15student2 = Student('Jane', 'Smith')
16
17print(student1.fname) # will work
18print(student1.__fname) # private, will NOT work

11.4.3. Static method

You can define functions at the class level, as opposed to the instance level, by annotating a function with @staticmethod. Such function is said to be a static method and is defined without using self as an argument since the function is not associated with an instance, but, rather, with the class. You do not need an instance of a class to call static methods, you can access a static method through the class name using dot . notation.

 1class Car(object):
 2    def __init__(self, make, model, year):
 3        self.make = make
 4        self.model = model
 5        self.year = Car.check_year(year)
 6
 7    @staticmethod
 8    def check_year(year):
 9        return 2019 if year < 1950 else year
10
11    def get_make(self):
12        return self.make
13
14    def get_model(self):
15        return self.model
16
17    def get_year(self):
18        return self.year
19
20    def set_make(self, make):
21        self.make = make
22
23    def set_model(self, model):
24        self.model = model
25
26    def set_year(self, year):
27        self.year = Car.check_year(year)
28
29
30car = Car('Honda', 'Accord', 2019)
31
32car.set_make('Toyota')
33car.set_model('Supra')
34car.set_year(1800)
35
36print(f'{car.get_make()} {car.get_model()} {car.get_year()}')

11.4.4. Method overriding dunders

Overriding a method means to redefine it (assumes that a method has been previously defined). When defining a class, two dunder functions that should be overridden are __str__() and __repr__(). Both methods return a string representation of the instance (of a class), however,

  • __str__() returns an informal representation, and

  • __repr__() returns a formal representation.

By convention, the string returned by __repr__() should be able to be used to reconstruct the instance. Many times, __str__() can just call __repr()__ (or vice-versa).

 1class Car(object):
 2    def __init__(self, make, model, year):
 3        self.make = make
 4        self.model = model
 5        self.year = year
 6
 7    def __repr__(self):
 8        return f"{{'make': {self.make}, 'model': {self.model}, 'year': {self.year}}}"
 9
10    def __str__(self):
11        return f'Car(make={self.make}, model={self.model}, year={self.year})'
12
13
14car = Car('Honda', 'Accord', 2019)
15
16print(car)
17print(repr(car))

11.5. Inheritance

One of the core tenets of coding is to reuse existing code. Why? Writing code is hard and testing code to assure quality and correctness is resource intensive. When you can, reuse code. This core tenent is so highly valued, we create languages like Python with features like OOP to encourage and enable code reuse. Specifically, inheritance is the feature of OOP that maximizes code-reuse. If we define a base class, there may be many sub-classes that share the same properties and actions, but with slight caveats. Inheritance enables the sub-classes to acquire the properties and actions of the base or parent class, and we can extend or modify the parent class’ features by adding or overriding its features.

Let’s understand inheritance a bit better with an example dealing with shapes. In particular, we want to model a rectangle as follows. A rectangle has a width and length (properties). A rectangle should be able to compute its own area and perimeter (actions).

  • Rectangle
    • properties
      • width

      • length

    • actions
      • computes area

      • computes perimeter

A Rectangle class definition could look like the following.

 1class Rectangle(object):
 2   def __init__(self, width, length):
 3      self.name = type(self).__name__
 4      self.width = width
 5      self.length = length
 6
 7   def get_area(self):
 8      return self.width * self.length
 9
10   def get_perimeter(self):
11      return self.width * 2 + self.length * 2
12
13   def __str__(self):
14      return self.__repr__()
15
16   def __repr__(self):
17      return f'{self.name}[width={self.width}, length={self.length}]'
18
19r = Rectangle(10, 5)
20print(r)
21print(r.get_area())
22print(r.get_perimeter())

Now we want to create a class definition for a square. We know that a square is a rectangle with all sides equaled to one another. We do not want to recreate all the logic of that goes into computing the area and perimeter of a square since we have that logic defined in the Rectangle class. We should use inheritance to achieve the goal of logical relationship and code reuse. In the Square class definition, the constructor is different by requiring only the value of one side (all sides are equal, or width is the same as length). Also, we use super() to call the ctor of Rectangle; Rectangle is called the base, parent or super class of Square. Lastly, notice how Square does not have to define get_area(), get_perimeter(), __str__() or __repr__()? Square has inherited such methods from Rectangle.

1class Square(Rectangle):
2   def __init__(self, side):
3      super().__init__(side, side)

Here is the all-in-one example of class inheritance.

 1class Rectangle(object):
 2   def __init__(self, width, length):
 3      self.name = type(self).__name__
 4      self.width = width
 5      self.length = length
 6
 7   def get_area(self):
 8      return self.width * self.length
 9
10   def get_perimeter(self):
11      return self.width * 2 + self.length * 2
12
13   def __str__(self):
14      return self.__repr__()
15
16   def __repr__(self):
17      return f'{self.name}[width={self.width}, length={self.length}]'
18
19class Square(Rectangle):
20   def __init__(self, side):
21      super().__init__(side, side)
22
23r = Rectangle(10, 5)
24print(r)
25print(r.get_area())
26print(r.get_perimeter())
27
28s = Square(5)
29print(s)
30print(s.get_area())
31print(s.get_perimeter())

Note

In OOP, much discussion is around differentiating between inheritance and composition. Inheritance is when one class (sub-class) derives from another class (base class). Composition is when one class has another class as one of its properties. Along with our Car class, we can also create a Wheel class, and specify that a Car has 4 Wheel. Two classes in an inheritance relationship is called a is-a relationship and two classes in a compos relationship is called a has-a relationship.

11.6. Abstract Classes

Here, we have a base class Animal. Noticed how the Animal class itself inherits from ABC (Abstract Base Class)? We annotate the make_noise method @abstractmethod decorator. A class that inherits from ABC cannot be instantiated directly. We create two subclasses, Dog and Cat, from Animal. These derived classes can be instantiated, however, they must implement all methods annotated with @abstractmethod or a TypeError will be thrown. The Animal class is also called a formal interface.

 1from abc import ABC, abstractmethod
 2
 3class Animal(ABC):
 4    def __init__(self, name):
 5        self.name = name
 6
 7    @abstractmethod
 8    def make_noise(self):
 9        pass
10
11class Dog(Animal):
12    def __init__(self, name):
13        super().__init__(name)
14
15    def make_noise(self):
16        print(f'{self.name} says woof')
17
18class Cat(Animal):
19    def __init__(self, name):
20        super().__init__(name)
21
22    def make_noise(self):
23        print(f'{self.name} says meow')
24
25animals = [Dog('clifford'), Cat('heathcliff')]
26for animal in animals:
27    animal.make_noise()

11.7. Informal Interface

An informal interface is defined like a class but has no internal state and method implementations are not provided. Below, we have a circle calculator which computes the area and circumference of a circle given the radius. We also show how to implement the informal interface.

 1class CircleCalculator(object):
 2    def get_area(self, radius):
 3        raise NotImplementedError
 4
 5    def get_circumference(self, radius):
 6        raise NotImplementedError
 7
 8class CircleCalculatorLogger(CircleCalculator):
 9    def get_area(self, radius):
10        area = 3.1415 * radius * radius
11        print(f'radius = {radius:.5f}, area = {area:.5f}')
12        return area
13
14    def get_circumference(self, radius):
15        circumference = 2.0 * radius * 3.1415
16        print(f'radius = {radius:.5f}, circumference = {circumference:.5f}')
17        return circumference
18
19calc = CircleCalculatorLogger()
20calc.get_area(3)
21calc.get_circumference(3)

11.8. Mixin

A mixin is like an abstract base class, but does not have state. It sits somewhere between an informal interface and abstract base class. Philosophically, a mixin avoids problems with single and multiple inheritance. Single inheritance can be taken to an extreme where long chains of inheritance can obfuscate the intentions of methods and properties (fragmentation). Multiple inheritance is problematic when diamond dependencies are created.

Below, we have an example class DivisionSolver that solves a division problem; the dividend (numerator) and divisor (denominator) must be given. The DivFloatMixin returns the solution as a float. The DivQRMixin returns the solution with the quotient and remainder.

 1class DivFloatMixin(object):
 2    def div_float(self):
 3        return self.dividend / self.divisor
 4
 5class DivQRMixin(object):
 6    def div_qr(self):
 7        return (self.dividend // self.divisor, self.dividend % self.divisor)
 8
 9# in Python, class hierarchy is defined right to left
10# object is the base class since it comes last
11# mixins should be defined first
12# if mixins override each other's methods, the priority is resolved left to right
13class DivisionSolver(DivQRMixin, DivFloatMixin, object):
14    def __init__(self, dividend, divisor):
15        self.dividend = dividend
16        self.divisor = divisor
17
18solver = DivisionSolver(11, 5)
19answer = solver.div_float()
20print(f'{answer:0.5f}')
21
22quotient, remainder = solver.div_qr()
23print(f'{quotient}r{remainder}')

Python has a lot of mixins. Below, we use the Mapping mixin to model a person’s mailing label. Notice that the MailingLabel class behaves like a dictionary?

 1from collections.abc import Mapping
 2import textwrap
 3import inspect
 4
 5class MailingLabel(Mapping):
 6    def __init__(self, **kwargs):
 7        self.data = kwargs
 8
 9    def __getitem__(self, key):
10        return self.data[key]
11
12    def __iter__(self):
13        return iter(self.data)
14
15    def __len__(self):
16        return len(self.data)
17
18    def __repr__(self):
19        s = f"""
20        {self.data['fname']} {self.data['lname']}
21        {self.data['street']}
22        {self.data['city']}, {self.data['state']} {self.data['zip']}
23        """
24        s = inspect.cleandoc(s)
25        return s
26
27label = MailingLabel(**{
28    'fname': 'One-Off',
29    'lname': 'Coder',
30    'street': '7526 Old Linton Hall Road',
31    'city': 'Gainesville',
32    'state': 'VA',
33    'zip': '20155'
34})
35
36print(label)
37print('-'*5)
38for k, v in label.items():
39    print(f'{k}: {v}')

11.9. Exercise

Model a teacher and students in a classroom. The class definitions required are as follows.

  • Person
    • properties
      • first name

      • last name

      • gender

    • actions
      • a check to see if the person is a male

  • Teacher
    • properties
      • a list of students

      • course name

    • actions
      • a way to count the number of students

      • a way to get the average grade of all students

      • a way to get the average grade of all male students

      • a way to get the average grade of all female students

  • Student
    • properties
      • a list of numeric grades

    • actions
      • a way to return the list of numeric grades as a list of letter grades

      • a way to get the overall grade numerically

      • a way to get the overall letter grade

Create instances of a Teacher and a list of Students with the following data.

  • What is the number grade of each student?

  • What is the letter grade of each student?

  • What is the average grade of all students of the teacher?

  • What is the average grade of all male students?

  • What is the average grade of all female students?

 1def get_teacher_data():
 2   return {
 3      'first_name': 'Janet',
 4      'last_name': 'Wang',
 5      'gender': 'female',
 6      'course': 'Intro to Python'
 7   }
 8
 9def get_student_data():
10   import random
11   random.seed(37)
12   get_grades = lambda lower, upper: [random.randrange(lower, upper) for _ in range(10)]
13
14   return [
15      {'first_name': 'Jack', 'last_name': 'Smith', 'gender': 'Male', 'grades': get_grades(75, 85)},
16      {'first_name': 'Joe', 'last_name': 'Johnson', 'gender': 'Male', 'grades': get_grades(85, 95)},
17      {'first_name': 'Jeremy', 'last_name': 'Zhang', 'gender': 'Male', 'grades': get_grades(55, 100)},
18      {'first_name': 'Justin', 'last_name': 'Ali', 'gender': 'Male', 'grades': get_grades(75, 90)},
19      {'first_name': 'Jeff', 'last_name': 'McDaniel', 'gender': 'Male', 'grades': get_grades(75, 90)},
20      {'first_name': 'Nancy', 'last_name': 'Wu', 'gender': 'Female', 'grades': get_grades(75, 100)},
21      {'first_name': 'Norah', 'last_name': 'Cortez', 'gender': 'Female', 'grades': get_grades(85, 100)},
22      {'first_name': 'Natasha', 'last_name': 'Canseco', 'gender': 'Female', 'grades': get_grades(80, 100)},
23      {'first_name': 'Natalie', 'last_name': 'Ronaldo', 'gender': 'Female', 'grades': get_grades(60, 100)},
24      {'first_name': 'Noella', 'last_name': 'Kim', 'gender': 'Female', 'grades': get_grades(90, 100)}
25   ]
26
27def get_data():
28   return {
29      'teacher': get_teacher_data(),
30      'students': get_student_data()
31   }

Solution.

  1def get_teacher_data():
  2   return {
  3      'first_name': 'Janet',
  4      'last_name': 'Wang',
  5      'gender': 'female',
  6      'course': 'Intro to Python'
  7   }
  8
  9def get_student_data():
 10   import random
 11   random.seed(37)
 12   get_grades = lambda lower, upper: [random.randrange(lower, upper) for _ in range(10)]
 13
 14   return [
 15      {'first_name': 'Jack', 'last_name': 'Smith', 'gender': 'Male', 'grades': get_grades(75, 85)},
 16      {'first_name': 'Joe', 'last_name': 'Johnson', 'gender': 'Male', 'grades': get_grades(85, 95)},
 17      {'first_name': 'Jeremy', 'last_name': 'Zhang', 'gender': 'Male', 'grades': get_grades(55, 100)},
 18      {'first_name': 'Justin', 'last_name': 'Ali', 'gender': 'Male', 'grades': get_grades(75, 90)},
 19      {'first_name': 'Jeff', 'last_name': 'McDaniel', 'gender': 'Male', 'grades': get_grades(75, 90)},
 20      {'first_name': 'Nancy', 'last_name': 'Wu', 'gender': 'Female', 'grades': get_grades(75, 100)},
 21      {'first_name': 'Norah', 'last_name': 'Cortez', 'gender': 'Female', 'grades': get_grades(85, 100)},
 22      {'first_name': 'Natasha', 'last_name': 'Canseco', 'gender': 'Female', 'grades': get_grades(80, 100)},
 23      {'first_name': 'Natalie', 'last_name': 'Ronaldo', 'gender': 'Female', 'grades': get_grades(60, 100)},
 24      {'first_name': 'Noella', 'last_name': 'Kim', 'gender': 'Female', 'grades': get_grades(90, 100)}
 25   ]
 26
 27def get_data():
 28   return {
 29      'teacher': get_teacher_data(),
 30      'students': get_student_data()
 31   }
 32
 33class Person(object):
 34   def __init__(self, first_name, last_name, gender):
 35      self.first_name = first_name
 36      self.last_name = last_name
 37      self.gender = gender
 38
 39   def is_male(self):
 40      return self.gender.lower() == 'male'
 41
 42class Teacher(Person):
 43   def __init__(self, first_name, last_name, gender, course, students=[]):
 44      super().__init__(first_name, last_name, gender)
 45      self.course = course
 46      self.students = students
 47
 48   @staticmethod
 49   def convert_number_to_letter(number):
 50      if 88.5 <= number <= 100.0:
 51            return 'A'
 52      elif 78.5 <= number < 88.5:
 53            return 'B'
 54      elif 68.5 <= number < 78.5:
 55            return 'C'
 56      elif 58.5 <= number < 68.5:
 57            return 'D'
 58      else:
 59            return 'F'
 60
 61   def get_num_students(self):
 62      return len(self.students)
 63
 64   def get_average_grade(self):
 65      grades = [s.grade for s in self.students]
 66      total = sum(grades)
 67      n = len(grades)
 68      average = total / n
 69      return average
 70
 71   def __get_average_grade_by_gender(self, gender='Male'):
 72      grades = [s.grade for s in self.students if s.gender == gender]
 73      total = sum(grades)
 74      n = len(grades)
 75      average = total / n
 76      return average
 77
 78   def get_male_average_grade(self):
 79      return self.__get_average_grade_by_gender()
 80
 81   def get_female_average_grade(self):
 82      return self.__get_average_grade_by_gender(gender='Female')
 83
 84   def get_student_grades(self, letter=True):
 85      return [(s.first_name, s.last_name, s.letter_grade if letter else s.grade) for s in self.students]
 86
 87class Student(Person):
 88   def __init__(self, first_name, last_name, gender, grades=[]):
 89      super().__init__(first_name, last_name, gender)
 90      self.grades = grades
 91      self.letter_grades = [Teacher.convert_number_to_letter(g) for g in grades]
 92      self.grade = sum(self.grades) / len(self.grades)
 93      self.letter_grade = Teacher.convert_number_to_letter(self.grade)
 94
 95data = get_data()
 96
 97to_student = lambda s: Student(s['first_name'], s['last_name'], s['gender'], s['grades'])
 98students = [to_student(s) for s in data['students']]
 99
100teacher = Teacher(
101   data['teacher']['first_name'],
102   data['teacher']['last_name'],
103   data['teacher']['gender'],
104   data['teacher']['course'], students)
105
106print(teacher.get_student_grades(letter=False))
107print(teacher.get_student_grades())
108print(teacher.get_average_grade())
109print(teacher.get_male_average_grade())
110print(teacher.get_female_average_grade())

11.10. Data classes

Data classes are available in Python 3.7. Notice the following:

  • the @dataclass annotation is placed on the class

  • properties defined in a data class requires type hints

  • the __eq__ and __repr__ dunders are implemented

 1from dataclasses import dataclass
 2
 3@dataclass
 4class Person:
 5   first_name: str
 6   last_name: str
 7   age: int
 8
 9p1 = Person('john', 'doe', 23)
10p2 = Person('jane', 'smith', 22)
11
12print(p1)
13print(p2)
14
15print(p1 == p2)