Primitive Types

We have learned about 5 primitive types: Booleans, Integers, Floats, Strings, and the special None. We can use these to represent many kinds of data, but they have a limitation since they are so simple and cannot be combined with each other. To increase our capabilities, we will learn about a new category of types, which is Composite types. Composite types are named because they are composed of other types, unlike primitive types. The first kind of composite types we will learn about are Dataclasses.

Dataclasses

Dataclasses are a data structure that lets us combine other types together to make a new type. You should not think of Dataclass as a type itself; instead, it is a way of creating new types. Or put another way, dataclasses are a kind of type, without being a type themselves.

An Example Dataclass

# Required import
from dataclasses import dataclass

# Definition
@dataclass
class Box:
    width: int
    length: int

# Instance creation
my_box = Box(5, 10)
# Using the instance
print(my_box)

Let’s look at an example Dataclass. First, we must import a decorator from the built-in dataclasses module (one of the many built-in libraries that Python conveniently provides for us). Then we create a definition for the dataclass by writing the header and body of the dataclass. The header requires the decorator, the class keyword, the name of the new dataclass, and a colon. The body requires that we specify the fields, each on their own line. Once defined, we can create instances of the dataclass using the constructor function, which has the same name as the defined dataclass. Storing the result in a variable allows us to use the instance later, such as shown in the following example where we print the my_box variable. Let’s break down each part of this example further!

@dataclass Decorator

from dataclasses import dataclass

@dataclass
class Dog:
    pass

Perhaps the most confusing part of dataclasses is the decorator. The decorator is the @dataclass annotation. The “at” symbol that we see in front of the word “dataclass” is the syntax that makes this a decorator. Basically, a decorator adds extra functionality on top of classes — in this case to make the classes into dataclasses. The decorated dataclasses are far more powerful and convenient than regular classes.

There’s a lot that can go wrong when using a decorator. You must import the dataclass decorator from the dataclasses module without including the @ symbol. However, when you put the decorator above the class definition, you absolutely must include the @ symbol. Messing up either part will prevent Python from understanding that you want to create a dataclass.

Dataclass Fields

from dataclasses import dataclass

@dataclass
class Dog:
    name: str
    age: int
    is_fuzzy: bool

The fields of a dataclass are the most useful part because they allow us to bundle up related data into a single place. Each field is like a variable inside of the dataclass. The rules for naming fields are the same as the rules for naming variables. First, the name must have only letters, digits, and underscores. Second, the name must only begin with letters or underscores. The rules for specifying the type of fields are like the rules for specifying the type of function parameters, except instead of commas, each field is on its own line.

The Constructor Function

from dataclasses import dataclass

@dataclass
class Dog:
    name: str
    age: int
    is_fuzzy: bool

ada = Dog("Ada Bart", 4, True)

Once a dataclass is defined, we can create any number of instances that represent concrete versions of the original dataclass. Instances of a dataclass are created by calling the constructor function. The constructor is a special function created by the dataclass definition and given the same name. The similarity between defining a function and defining a dataclass is very strong. The similarity between calling a function and instantiating (“creating an instance of”) a dataclass is even stronger. Each field of the dataclass definition corresponds to an argument for the constructor. The order and type of each field must match the order and type of each argument.

Dataclass vs Instance

A very confusing concept is what a dataclass represents compared to an instance of the dataclass. A similar concept is the difference between a stencil and a drawing. Another similar concept is the difference between a recipe and a cake. In all these cases, one of these is an idea, something totally theoretical. The other is a concrete thing.

Dog vs Instance of Dog

A Dog dataclass definition (an outline sketch of a dog next to its fields, name, age, and fuzzy) with arrows pointing to two concrete Dog instances, each shown with a photo of a real dog next to the constructor call that creates it.
Dog icon: OpenMoji, designed by OpenMoji, the open-source emoji and icon project, CC BY-SA 4.0, mirrored.

Let us consider the Box example from before. When you only look at the definition of the Box, it is impossible to say what width a Box is. You can generally say that the width of a Box will be an integer value. But there is no specific integer value associated with the idea of a Box. Instead, you would need to create an instance of a Box to talk meaningfully about the width of that box. The concept of a Box does not have a width. You can create an instance using the constructor function that has a width of 5 or 100, but the original Box object itself does not inherently have a width. The idea for dogs shown here is similar, with a distinction between the idea of a Dog and the specific values of real dogs. There is no image for the abstract dataclass definition of a dog, other than a vague outline. However, the two real dog instances can have photos associated, because they model actual real dogs.

Accessing Fields

from dataclasses import dataclass

@dataclass
class Dog:
    name: str
    age: int
    is_fuzzy: bool

ada = Dog("Ada Bart", 4, True)
print("Ada's name is", ada.name)

One of the most critical parts of using an instance is to access its fields. The period and the name of the field are used to access those fields. Generally, you can think of a field as a variable that lives inside of an instance. In the code shown here, we access the name field of the Dog instance stored in ada. A critical thing to understand, however, is that you can only access the fields of an instance, not a dataclass. In other words, you cannot try to access the fields of the Dog dataclass, only the ada instance. If you try changing the code, you will see that you get an error when you try to access Dog.name instead of ada.name. It’s very important to understand the idea that the Dog is an abstract idea, while the ada instance is a real concrete bit of data.

Terminology of Fields

There are many words that mean roughly the same thing when it comes to dataclasses. One concept that has many synonyms is the term field, which can be used interchangeably with attribute, property, and member. We will use the terms field and attribute most often, although occasionally we will refer to properties or members.

Tracing Dataclasses

A diagram labelled Variables and Values, above a snippet of code. The code is two lines long, with the first line being a call to the constructor in order to create an instance of a Dog dataclass and store it in the variable `ada`. The second line of code is an assignment of the Dog instance's `name` field's value to a `my_dogs_name` variable. The diagram models the representation in memory, including an arrow pointing from the `ada` variable to the Dog Instance on the right hand side of Values.

The data stored in a dataclass is more complicated than the data stored in a simple variable. There are multiple pieces of data (the fields), all stored in a single object. Conceptually, we say that the data for an instance lives in an area of memory called the heap, as opposed to living in the “stack” that we learned about when tracing function calls. The heap is a special region of memory with many interesting properties that we will explore over time. Variables on the stack can hold references to values on the heap. For now, we can use a stack/heap diagram like the one below to visualize the relationship between the variables and data on the stack and the data on the heap.

In this stack/heap diagram, you can see how dataclasses are traced a little differently compared to regular primitive types of data. When the constructor is called, a new instance of a dataclass is created. That instance is a special kind of value with multiple fields which can be seen on the right-hand side of the diagram. When code accesses one of those fields, like on the second line (ada.name), the arrow is followed and the named field’s value is reached. That value is a simple primitive string and so can be stored directly in the my_dogs_name variable. The ada variable points to the entire Dog instance, unlike the my_dogs_name variable which points to a simple primitive value. For now, all we need to do is keep track of the difference between primitive values stored in the stack and special instances stored on the heap. But, eventually, we will develop a deeper understanding of how data is laid out.