DEV Community

Mark Edosa
Mark Edosa

Posted on

Introduction to Python Programming - Classes

Table of Contents

Introduction

Classes are a classic feature of objected-oriented programming, a programming paradigm that structures code as objects with attributes (variables) and behavior (functions or methods). The attributes and functions usually form a single logical unit. For example, you may define a Person class to represent the attributes and behavior of a person.

You can also see a class as a blueprint for creating objects, similar to how architectures are blueprints for making concrete buildings. The objects are called instances of that class.

Unlike object-oriented languages like Java, Python lets you use classes or not. You can choose between using the variables and functions individually or group them under a class.

Before proceeding, I assume you understand basic Python programming, including variables and functions.

Creating a Class

Use the class keyword followed by the class name and a colon. For example:

class Restaurant:
    # ...
Enter fullscreen mode Exit fullscreen mode

While you can use any valid naming pattern to define your class names, by convention, classes are defined using Pascal cases. For example, Animal, MyBook, ItalianRestaurant, etc.

Next, include the attributes and methods that are either attached to the class itself or the instances of the class. For example:

class Restaurant:
    """A restaurant class"""

    count: int = 0

    def __init__(self) -> None:
        self.name: str = ''
        self.address: str = ''
        self.type: str = ''
        self.capacity: int = 1
        self.spacing: float = 1.0


    def get_name(self) -> str:
        return self.name

    def set_name(self, name: str) -> None:
        self.name = name

    @classmethod
    def increase_count(cls) -> None:
        cls.count += 1


    @staticmethod
    def describe() -> None:
        print('My awesome Restaurant')
Enter fullscreen mode Exit fullscreen mode

The Restaurant class has the following:

  • A docstring """A restaurant class""" accessible through __doc__ attribute
  • A class attribute count, an integer directly attached to the Restaurant class and shared by all instance objects.
  • An __init__() method where instance variables are created and or initialized
  • Five instance attributes or variables - name, address, type, capacity, and spacing are attached to each unique instance of the Restaurant class.
  • Two instance methods - get_name() and set_name(). An instance method must take itself (self) as its first argument.
  • A class method increase_count() which is attached to the Restaurant class. A class method must take the class itself as its first argument.
  • An independent static method describe() which is in the class basically for the sake of grouping. This means that static methods can exist as regular functions outside the class. Note that you can access a static method via the class object and an instance object.

In Python, a class, like its instances, is also an object.

Defining a class automatically a class object. You can access its attributes using the ClassName.name syntax. For example:

# Print some of the class attributes
print(Restaurant.count)
print(Restaurant.__doc__)
print(Restaurant.increase_count)
print(Restaurant.describe)

# You can also add new attributes to a class
# However, do not do this. It is better to include
# your variables in the class definition
Restaurant.age = 10
print(Restaurant.age)

# Delete the added attribute 
del Restaurant.age
# print(Restaurant.age) # Throws AttributeError

# call the class method
Restaurant.increase_count()
print(Restaurant.count)

# call the static method
Restaurant.describe()
Enter fullscreen mode Exit fullscreen mode
0
A restaurant class
<bound method Restaurant.increase_count of <class '__main__.Restaurant'>>
<function Restaurant.describe at 0x000002882CECE3E0>
1
My awesome Restaurant
Enter fullscreen mode Exit fullscreen mode

0x000002882CECE3E0 is the current memory address of the function Restaurant.describe(). The memory address usually changes each time you run the Python program.

Creating Instance Objects

You can also create an instance object using the ClassName() syntax. For example:

italian_rest = Restaurant()

# Print the instance object
# Prints the memory address at the time of creation
print(italian_rest)
# Check the type
print(type(italian_rest))

# Create another independent instance of Restaurant
chinese_res = Restaurant()
print(type(chinese_rest))

# Instances are independent or unique
print(italian_rest == chinese_res)
Enter fullscreen mode Exit fullscreen mode
<__main__.Restaurant object at 0x00000229E1AB78F0>
<class '__main__.Restaurant'>
<class '__main__.Restaurant'>
False
Enter fullscreen mode Exit fullscreen mode

Checking the types shows that both italian_rest and chinese_rest objects are Restaurant objects. Also, you can use the built-in isinstance() function to determine if an object was created from a class. For example:

print(isinstance(italian_rest, Restaurant))
print(isinstance(chinese_res, Restaurant))
print(isinstance(chinese_res, list))
Enter fullscreen mode Exit fullscreen mode
True
True
False
Enter fullscreen mode Exit fullscreen mode

The instance variables and methods (including static methods) can be accessed using the instance.name syntax. For example:

print(italian_rest.capacity)
print(italian_rest.spacing)

italian_rest.describe()
print(italian_rest.name) # empty string
italian_rest.set_name('Italian restaurant')
print(italian_rest.name)
Enter fullscreen mode Exit fullscreen mode
1
1.0
My awesome Restaurant

Italian restaurant
Enter fullscreen mode Exit fullscreen mode

Class and Instance Variables

Looking at the Restaurant class, count is a class variable while name, address, and so on are instance variables.

All instances of a class share class variables while instance variables are unique to each instance object created from the class. Each time you create a new instance of Restaurant, a call to Restaurant.increase_count() or self.increase_count() within the __init__() method increases count by one.

class Restaurant:
    """A restaurant class"""

    count: int = 0

    def __init__(self) -> None:
        self.name: str = ''
        self.address: str = ''
        self.type: str = ''
        self.capacity: int = 1
        self.spacing: float = 1.0

        Restaurant.increase_count()
        # self.increase_count()

    # ... Elided for brevity


italian = Restaurant()
chinese = Restaurant()

print(Restaurant.count) # 2
print(italian.count) # 2
print(chinese.count) # 2
Enter fullscreen mode Exit fullscreen mode

Note that while count also appears to be accessible via the instances - chinese and italian, it's not an instance variable. The __dict__ attribute (a dictionary) of an instance contains the variables attached to an instance.

print(italian.__dict__)
print(chinese.__dict__)
Enter fullscreen mode Exit fullscreen mode
{'name': '', 'address': '', 'type': '', 'capacity': 1, 'spacing': 1.0}
{'name': '', 'address': '', 'type': '', 'capacity': 1, 'spacing': 1.0}
Enter fullscreen mode Exit fullscreen mode

count is not in __dict__. You could "hide" or override a class variable by adding the same variable directly to an instance object. Don't do this:

italian.count = 3

print(italian.count) # 3
print(chinese.count) # 2
print(italian.__dict__)
print(chinese.__dict__)

Enter fullscreen mode Exit fullscreen mode

A new instance variable count on the italian instance now overshadows the class variable count.

3
2
{'name': '', 'address': '', 'type': '', 'capacity': 1, 'spacing': 1.0, 'count': 3}
{'name': '', 'address': '', 'type': '', 'capacity': 1, 'spacing': 1.0}
Enter fullscreen mode Exit fullscreen mode

Deleting the instance count makes the class count re-appear.

del italian.count
print(italian.count) # 2
print(italian.__dict__)
# {'name': '', 'address': '', 'type': '', 'capacity': 1, 'spacing': 1.0}
Enter fullscreen mode Exit fullscreen mode

In Python, instance variables, like class variables, must be initialized upon creation inside the __init__() method. The string variables such as name are currently empty strings '' while capacity and spacing are int and float types, respectively. You could modify the instance variables after creation. For example:

from pprint import pp # pretty print

print(italian.name)
print(italian.address)

italian.name = 'Francesco T. Burgers'
italian.address = "21st Navy Avenue, Italy."
italian.capacity = 80
italian.type = 'Italian restaurant'
italian.spacing = 18.2

print(italian.__dict__)
print(italian.name)
print(italian.address)
print(italian.capacity)
Enter fullscreen mode Exit fullscreen mode


{'name': 'Francesco T. Burgers',       
 'address': '21st Navy Avenue, Italy.',
 'type': 'Italian restaurant',
 'capacity': 80,
 'spacing': 18.2}
Francesco T. Burgers
21st Navy Avenue, Italy.
80
Enter fullscreen mode Exit fullscreen mode

Modifying each instance variable feels like a lot of work. Luckily, the __init__() can take any number of arguments you can use to set instance variables. For example:

class Restaurant:
    # ...

    def __init__(self, name: str, address: str, type: str,
                 capacity: int = 1, spacing: float = 1.0):
        self.name = name
        self.address = address
        self.type = type
        self.capacity = capacity
        self.spacing = spacing

        Restaurant.increase_count()

    # ...
Enter fullscreen mode Exit fullscreen mode

You can now create an instance and set appropriate values upon creation. Recreating the italian restaurant you get:

italian = Restaurant('Francesco T. Burgers', '21st Navy Avenue, Italy.',
                     'Italian restaurant', 80, 18.2)

print(italian.name)
print(italian.address)
print(italian.capacity)
print(italian.type)
print(italian.spacing)
Enter fullscreen mode Exit fullscreen mode
Francesco T. Burgers
21st Navy Avenue, Italy.
80
Italian restaurant
18.2
Enter fullscreen mode Exit fullscreen mode

How about the chinese instance:

chinese = Restaurant('Jet Li Spot', '31st Broadway Palace',
                     'Chinese restaurant', 80, 16.2)


print(chinese.name)
print(chinese.address)
print(chinese.capacity)
print(chinese.type)
print(chinese.spacing)
Enter fullscreen mode Exit fullscreen mode
Jet Li Spot
31st Broadway Palace
80
Chinese restaurant
16.2
Enter fullscreen mode Exit fullscreen mode

Using Dunder init for Class Invariance

You can also set constraints on instantiating a class in the __init__() method. For example, it might not make sense for a restaurant to have no name or address or have a capacity or spacing of zero. You can include checks to ensure that nobody can create such a class.

The concept is known as establishing class invariance. A state where an object of a class meets certain expectations that never change throughout its lifetime. For example, we use the __init__() to ensure Restaurants are never created with empty strings or with zero(0) and negative spacing and capacity.

class Restaurant:
    """A restaurant class"""

    count = 0

    def __init__(self, name: str, address: str, type: str,
                 capacity: int = 1, spacing: float = 1.0):

        invalid_name = self._invalid_str(name)
        invalid_address = self._invalid_str(address)
        invalid_type = self._invalid_str(type)

        if invalid_name or invalid_address or invalid_type:
            raise ValueError('Please provide a non-empty string for name, address and type')

        if capacity <= 0 or spacing <= 0:
            raise ValueError('capacity and spacing cannot be zero or less')


        self.name = name
        self.address = address
        self.type = type
        self.capacity = capacity
        self.spacing = spacing

        self.increase_count()

    # ...

    def set_name(self, name):
        if self._invalid_str(name):
            raise ValueError('Name must be string and cannot be empty')
        self.name = name


    def _invalid_str(self, value):
        return not isinstance(value, str) or value == ''

    # ... Elided for brevity
Enter fullscreen mode Exit fullscreen mode

The set_name() method also has a check to ensure non-violation of the class invariance.

# Works
Restaurant('Jet Li Spot', '31st Broadway Palace',
                     'Chinese restaurant', 80, 16.2)

# Invariance violated
# Fails: empty name, address
Restaurant('', '', 'Italian', 18, 17.3)
# ValueError: Please provide a non-empty string for name, address, and type

# Invariance violated
# Fails: capacity is 0
Restaurant('Jet Li Spot', '31st Broadway Palace',
                     'Chinese restaurant', 0, 16.2)
# ValueError: capacity and spacing cannot be zero or less
Enter fullscreen mode Exit fullscreen mode

Class and Instance Methods

A class method, like a class variable, is associated with a class. The method has an annotation @classmethod on top of it. Its first argument is the class itself. See the increase_count(cls, by) method defined earlier. You can use the word cls as the first argument since the class keyword is reserved.

You can use ClassName.class_method() within or outside the class or self.class_method() within the class. For example:

class Restaurant:
    # ... Elided for brevity

    def __init__(
        #  ... Elided for brevity
        ):

        # ... Elided for brevity

        # Restaurant.increase_count()
        self.increase_count()

    @classmethod
    def increase_count(cls):
        cls.count += 1


Restaurant.increase_count()
chinese.increase_count()
italian.increase_count()

print(Restaurant.count) # 5
Enter fullscreen mode Exit fullscreen mode

As you've seen above, each instance can also call a class method. However, all class instances share the result of the call.

Static Methods

A method marked with @staticmethod is a static method. As stated earlier, a static method can exist as a function outside a class. While a class or instance can call a static method, for example, MyClass.static_method() or my_instance.static_method(), the static method does not take the class cls or instance self as its first argument.

Essentially, it is not necessary to have a static method in a class. For example, the describe() method could be a regular function in the module:

def describe() -> None:
    print('My awesome Restaurant')

class Restaurant:
    # ...
Enter fullscreen mode Exit fullscreen mode

Data and Implementation Hiding

Unlike Python, some programming languages like C#, Java, and Cpp have public, private, and protected variables or methods. This helps programmers hide data (private variables) and implementation details (through private methods). To mimic this behavior, many Python programmers, by convention, prefix "private" variables and methods with underscores (_). For example:

class MyClass:

    def __init__():
        self._my_private_variable = 0


    def _my_private_method(self):
        pass

Enter fullscreen mode Exit fullscreen mode

While all instance methods and variables (whether prefixed or not) can still be accessed or modified. However, many people follow the convention. That is usually enough.

Inheritance

A class can inherit attributes and behavior from another class. This is known as inheritance in object-oriented programming. You usually want to do this to extend or modify the behavior of a class. The class inheriting is referred to as a "child" or "sub" or "derived" class while its parent, the "parent" or "base" class.

In Python, a base class can have one or multiple children or subclasses (single inheritance). A child class can have multiple parents (multiple inheritance). The syntax for inheritance in Python is class Child(ParentA, ..., ParentZ):

Single Inheritance

A class class can inherit from a single parent. All classes you create implicitly inherit from the built-in object class. This means that class Restaurant: is equivalent to class Restaurant(object):. For example:

class Restaurant(object):
    # ...
Enter fullscreen mode Exit fullscreen mode

Some people like to be explicit, but I don't think this is necessary.

Add a cook_meal() method to the Restaurant class. You will expect the implementation of this method in an italian instance to be different from a chinese instance or any other instance since they serve different meals.

class Restaurant:
    # ...

    def cook_meal():
        # raise NotImplementedError so that each
        # subclass must override this method
        raise NotImplentedError

Enter fullscreen mode Exit fullscreen mode

While you could use an if statement that checks the type of a restaurant, the easiest way to specialize the cook_meal() is to create subclasses representing each kind of restaurant. For example:

class ItalianRestaurant(Restaurant):

    def cook_meal(self):
        self._make_italian_dishes()


    def _make_italian_dishes(self):
        """Meant to be private"""
        print("I am making Carbonara, Lasagna, and Pizza!")


class ChineseRestaurant(Restaurant):

    def cook_meal(self):
        self._make_chinese_dishes()


    def _make_chinese_dishes(self):
        print("I am making Kung Pao Chicken, Fried Rice, and Hot Pot!")


class NigerianRestaurant(Restaurant):

    def cook_meal(self):
        self._make_nigerian_dishes()


    def _make_nigerian_dishes(self):
        print("I am making Egusi soup, Pounded yam, and Pepper soup")
Enter fullscreen mode Exit fullscreen mode

You can create new instances like you would a Restaurant class. The variable initialization stays the same. For example:

italian = ItalianRestaurant('Francesco T. Burgers', '21st Navy Avenue, Italy.',
                     'Italian restaurant', 80, 18.2)

chinese = ChineseRestaurant('Jet Li Spot', '31st Broadway Palace, China.',
                     'Chinese restaurant', 80, 16.2)


print(type(italian))
print(isinstance(italian, Restaurant))
print(isinstance(italian, ItalianRestaurant))
print(issubclass(ItalianRestaurant, Restaurant))

print(type(chinese))
print(isinstance(chinese, Restaurant))
print(isinstance(chinese, ChineseRestaurant))
print(issubclass(ChineseRestaurant, Restaurant))

Enter fullscreen mode Exit fullscreen mode
<class '__main__.ItalianRestaurant'>
True
True
True
<class '__main__.ChineseRestaurant'>
True
True
True
Enter fullscreen mode Exit fullscreen mode

Like the isinstance() function, you can use the issubclass() function to check if a class is a subclass.

You can now call the cook_meal() of each instance to get specialized meals. For example:

italian.cook_meal()
chinese.cook_meal()
Enter fullscreen mode Exit fullscreen mode
I am making Carbonara, Lasagna, and Pizza!
I am making Kung Pao Chicken, Fried Rice, and Hot Pot!
Enter fullscreen mode Exit fullscreen mode

Notice that each subclass has its own _make_<country>_dishes() unique to that class. The other variables and methods are inherited as is:

print(chinese.address)
print(chinese.type)
print(chinese.spacing)
print(chinese.get_name())
chinese.describe()
Enter fullscreen mode Exit fullscreen mode
31st Broadway Palace, China.
Chinese restaurant
16.2
Jet Li Spot
My awesome Restaurant
Enter fullscreen mode Exit fullscreen mode

Inheritance can be as deep as you want. That is to say that a child class can also be the base class of another class. For example:

class RomanRestaurant(ItalianRestaurant):
    pass


roman = RomanRestaurant('Roman T. Burgers', '21st Navy Avenue, Rome.',
                            'Italian restaurant', 80, 18.2)

# Type and instance checks
print(type(roman))
print(isinstance(roman, Restaurant))
print(isinstance(roman, ItalianRestaurant))
print(isinstance(roman, RomanRestaurant))

# Subclass checks
print(issubclass(RomanRestaurant, ItalianRestaurant))
print(issubclass(RomanRestaurant, Restaurant))
Enter fullscreen mode Exit fullscreen mode
<class '__main__.RomanRestaurant'>
True
True
True
True
True
Enter fullscreen mode Exit fullscreen mode

Too many levels deep can be problematic. Remember to keep it simple.

The super() Function

What if you need to add new instance variables to a derived class or call a parent class method within a derived class? Enter the super() function. For example:

class ParentClass:

    def __init__(self, name, age) -> None:
        self.name = name
        self.age = age


    def talk(self):
        print(f'My name is {self.name} and I am {self.age} years old!')


    def increase_age(self, by = 1):
        self.age += by



class ChildClass(ParentClass):

    def __init__(self, name, age, some_other_variable) -> None:
        # call the ParentClass __init__()
        super().__init__(name, age)

        self.some_other_variable = some_other_variable


    def talk(self):
        if self.age >= 2:
            super().talk()
        else:
            print('I cannot talk yet!')



father = ParentClass('John Doe', 945)
son = ChildClass('Peter Doe', 1, 'Whatever you like')

father.talk()
son.talk()
son.increase_age()
son.talk()
Enter fullscreen mode Exit fullscreen mode
My name is John Doe and I am 945 years old!
I cannot talk yet!
My name is Peter Doe and I am 2 years old!
Enter fullscreen mode Exit fullscreen mode
  • ChildClass derives from ParentClass.
  • Using super(), the ChildClass calls ParentClass's __init__() method to initialize the name and age instance variables. It (ChildClass) then creates and initializes some_other_variable specific to itself.
  • Within the overridden talk() method, the ChildClass calls the parents ParentClass's talk() method through the super() function if age is 2 or greater.

The super() function gives you access to the immediate base class. Let's add a GrandChildClass to the mix.

class GrandChildClass(ChildClass):

    def __init__(self, name, age, some_other_variable) -> None:
        super().__init__(name, age, some_other_variable)

    def talk(self):
        super().talk()
        print('I am a grandchild')


grandson = GrandChildClass("JC", 2, "Whatever")

grandson.talk()
Enter fullscreen mode Exit fullscreen mode
My name is JC and I am 2 years old!
I am a grandchild
Enter fullscreen mode Exit fullscreen mode

The super() in the grandchild points to ChildClass.

Multiple Inheritance

As stated earlier, a derived class can inherit from multiple base classes. For example, the StudentWorker class inherits from both Student and Worker classes.

class Student:

    def __init__(self, school) -> None:
        self.school = school

    def read(self, hours=1):
        print(f'I have read for {hours} hours!')

    def describe(self):
        print(f"I am a student at {self.school}")


class Worker:

    def __init__(self, occupation) -> None:
        self.occupation = occupation

    def work(self, hours=1):
        print(f'I have worked for {hours} hours!')

    def describe(self):
        print(f"I work as a {self.occupation}")


class StudentWorker(Student, Worker):

    def __init__(self, school, occupation) -> None:
        Student.__init__(self, school) # same as super().__init__(school)
        Worker.__init__(self, occupation)

    def work_and_read(self):
        super().work()
        super().read()
Enter fullscreen mode Exit fullscreen mode

A StudentWorker has the variables and methods defined in both base classes.

sw = StudentWorker("John Hopkins University", "Bartender")
sw.read()
sw.work()
sw.work_and_read()
Enter fullscreen mode Exit fullscreen mode
I have read for 1 hours!
I have worked for 1 hours!
I have worked for 1 hours!
I have read for 1 hours!
Enter fullscreen mode Exit fullscreen mode

work(), read(), and work_and_read() are unique to each class. So, the outcome is straightforward.

But what happens if the parents have the same methods? For example, both Student and Worker classes have a describe() method. What happens if you call sw.describe() or super().describe()?

If you guessed "I am a student" you are right! "I am a worker" is not.

sw.describe()
Enter fullscreen mode Exit fullscreen mode
I am a student at John Hopkins University
Enter fullscreen mode Exit fullscreen mode

However, you would get "I am a worker" if you swap the order of inheritance. That is StudentWorker(Worker, Student). The reason is that Python maintains a list of classes it checks (depth-first, then left-right) when searching for a method. This concept is known as Method Resolution Order (MRO).

The first item on this list is the class itself, next are the base classes ordered according to the class definition, and lastly, the object class, the mother of all classes. The mro() method gives you access to this list. For example:

print(StudentWorker.mro())
Enter fullscreen mode Exit fullscreen mode
[<class '__main__.StudentWorker'>, <class '__main__.Student'>, <class '__main__.Worker'>, <class 'object'>] 
Enter fullscreen mode Exit fullscreen mode

Therefore, when you call sw.describe() or super().describe() or any method in fact, Python will check in the following order and pick the first match:

  • StudentWorker
  • Student and its parents (since Student has its own MRO)
  • Worker and its parents (since Worker has its own MRO)
  • object

If Python does not find a method after these checks, it raises an AttributeError. See the article on Exceptions for more details.

Instead of swapping the order of inheritance, you could use the base class directly. For example, Worker.__init__(self, occupation) and Worker.describe(self) will initialize the occupation variable and call the Worker's describe() method, respectively.

Printing and Debugging An Object

When you print an instance using either print(), str(), format(), or repr(), you will get a somewhat cryptic representation of that class or instance. For example:

sw = StudentWorker()

print(sw)
print(str(sw))
print(repr(sw))
print(format(sw))
Enter fullscreen mode Exit fullscreen mode
<__main__.StudentWorker object at 0x0000027314327D40>
<__main__.StudentWorker object at 0x0000027314327D40>
<__main__.StudentWorker object at 0x0000027314327D40>
<__main__.StudentWorker object at 0x0000027314327D40>
Enter fullscreen mode Exit fullscreen mode

Luckily, you can get better and friendlier output by overriding the following methods inherited from the base object class:

  • __repr__(self) - which is called by the repr() function as well as the REPL at the command line
  • __str__(self) - which is called by both the str() and print() functions
  • __format__(self, format_spec) which is called by the format() function and the string .format() method

Let's override only __repr()__ and __str()__ since the format() function uses __str__() if it is present.

class StudentWorker(Student, Worker):
    # ... Elided for brevity

    def __repr__(self) -> str:
        return f"StudentWorker(school={self.school}, occupation={self.occupation})"


    def __str__(self) -> str:
        return f"Studies at {self.school} and works as a {self.occupation}"
Enter fullscreen mode Exit fullscreen mode

We now see a different outcome when we print the object using the same functions mentioned earlier.

sw = StudentWorker("John Hopkins University", "Bartender")

print(repr(sw)) # repr() calls __repr__
print("StudentWorker:", sw)
print(str(sw))
print(format(sw)) # format() calls __str__
print(f"He is serious! He {sw}") # format() calls __str__
Enter fullscreen mode Exit fullscreen mode
StudentWorker(school=John Hopkins University, occupation=Bartender)
StudentWorker: Studies at John Hopkins University and works as a Bartender
Studies at John Hopkins University and works as a Bartender
Studies at John Hopkins University and works as a Bartender
He is serious! He Studies at John Hopkins University and works as a Bartender
Enter fullscreen mode Exit fullscreen mode
  • Overriding __repr__() is useful for debugging. StudentWorker(school=John Hopkins University, occupation=Bartender) is better than <__main__.StudentWorker object at 0x0000027314327D40>
  • Overriding __str__() gives a friendlier message.

Data Classes

Data classes are useful for data grouping, representation, or transfer. A data class is a regular class decorated with dataclasses.dataclass. Like regular classes, it can take any number of variables (with types specified) and methods and supports single or multiple inheritance.

However, unlike the regular classes, a data class contains default implementations of __init__() and __repr() methods. For example:

from dataclasses import dataclass, field

@dataclass
class Person:
    name: str
    age: int

    # All keyword variables must come after the positional variables
    # Keyword variables: variables with default arguments
    occupation: str = 'none'

    # hobbies: list = [] # FAILs no mutable argument allowed
    hobbies: list[str] = field(default_factory=list)


john = Person('John Doe', 45, 'Bartender')

print(john)
Enter fullscreen mode Exit fullscreen mode
Person(name='John Doe', age=45, occupation='Bartender', hobbies=[])
Enter fullscreen mode Exit fullscreen mode
  • name and age are positional variables
  • occupation is a keyword variable with a default value of none.
  • hobbies is a list initialized to [] by a call field(). Direct initialization of mutable data types like dict and list is not allowed.

The __init__() and repr() implemented for you might look like these:

# ... Within the Person class
def __init__(self, name, age, occupation='none', hobbies=[]):
    self.name = name
    self.age = age
    self.occupation = occupation
    self.hobbies = hobbies


def __repr__(self) -> str:
    result = f'{type(self).__name__}(' # Person(

    # ', 'join() takes a generator expression
    # Generator expression yields variable=value
    result += ', '.join(f"{k}={v!r}" for k, v in self.__dict__.items())
    result += ')' # close it

    return result

# ... Within the Person Class
Enter fullscreen mode Exit fullscreen mode

You can get a glimpse of the implemented __init__() by looking at its signature.

import inspect

print(inspect.signature(john.__init__))
Enter fullscreen mode Exit fullscreen mode
(name, age, occupation='none', hobbies=[]) -> None
Enter fullscreen mode Exit fullscreen mode

Feel free to drop the __repr__(self) code above in any regular class you create. It just works! The type(self)__name__ ensures that a subclass returns the appropriate type, which is itself.

Every other thing we've discussed for regular classes applies to data classes. Check out the official Python documentation to learn more about data classes.

Note that Python also supports Enumerations (classes that provide unique values). Check the official documentation also on how to use enumerations.

Summary

In this article, you learn how to create a class and instances of classes. You also learned about class and instance variables and methods. You learned about single and multiple inheritance and how Python searches for methods (and variables). Finally, you saw how to create a data class.

Thank you for reading!

Top comments (0)