1 Introduction

In object-oriented programming (OOP), you can represent real-world things as objects with data and functions. This approach makes code more organized and reusable. In this style of programming, objects are created from classes, which act as blueprints for objects.

“Python” language is an object-oriented programming language, and classes are a fundamental part of object-oriented programming. Classes allow you to create new types of objects with their own attributes and methods. In this document, we will explore the concept of classes in “Python” language, their syntax, and usage. A class is a complicated concept for the beginners, so I will cover only the basics of it.

2 What is a Class?

In “Python” language, a class is a blueprint or a template for creating objects. It defines a data structure that describes the properties and behaviors of objects that will be created based on that class.

2.1 A Class and an Object

Through this article, I will often use the term “class” and “object”. If this is your first time to learn a class, the two words may be confusing. As I mentioned, a class is a blueprint for creating objects (instances), while an object is a specific instance of a class. However, what does that mean? A good example will help you grasp the concept.

Imagine that you are a teacher in a school. This is a new school year, and you will have some students in your class. You need to create student-information sheets to understand the details of them and make their school lives safe and fun. For this purpose, you created the following sheet:

3 Student Information Sheet

Full Name

Address

Parent / Guardian Information

What is your child interested in?

This empty sheet works as a blueprint or template. You will send this sheet to the parents or guardians of your students, and they will fill in the sheet. The filled-in sheets are the real container of your students’ information. Now, you can understand that the template and the filled-in sheets correspond to the class and objects, respectively.

TIPS: Metaphor for Understanding a Class and an Object

  • Empty student-information sheet: Class
  • Filled-in sheet: Object

In a nutshell, you needed a container for your students’ information. You modeled the container as the student-information sheet (class). Although this empty sheet is a template, the parents or guardians filled in the sheet and sent them back to you with the real information. These filled-in sheets work as the instance of the template with the real data. These are the objects. As you can see in this example, you will have multiple objects from a single class.

Also, it should be noted that objects are called instance in “Python” programming. So you will create an instance based on a class (@note-cla-obj-ins).

NOTE

Strictly speaking, a class is called a “class object” in “Python” language. In this mean, a class is probably an object in “Python” language. As far as I understand, however, in Object-Oriented programming, it is common practice to distinguish between a class and an object (instance). Therefore, I will treat them as separate concepts.

Now, it’s time to check how you define the class and how they work in “Python” programming. We will take a circle as an example and create a class.

4 Syntax for Defining a Class

First, we will check how to define a class. In “Python” language, the syntax for defining a class is as follows:

TIPS: Defining a Class

class ClassName:
    # Class body
    # Define attributes and methods here

The class keyword is used to define a new class, followed by the class name. The body of the class contains attributes (data) and methods (functions) that define the behavior of the class.

Using the class keyword, I create a class for a circle:

class Circle:
    pass

This class is empty, so I will add some attributes and methods.

5 Attributes and Methods

You have learned variables and functions in this crash course. As you know, variables are containers of data, and functions are set of instructions that you can call whenever you want.

A class or an object can have the variables and functions inside them. An attribute is a variable defined in a class, while a method is a function defined in a class. Let’s check the meaning of those terms in detail.

5.1 Attributes

An attribute is a variable defined in a class. If an attribute is shared across all instances of the class, it is called a class attribute. On the other hand, when the value of an attribute is not shared through the instances, it is called an instance attribute:

class Circle:
    # Class attribute
    object_type = "plane"

    # Constructor method that initializes a Circle object with the coordinates of the center and the radius
    def __init__(self, center, radius):
        # Instance attributes
        self.center = center
        self.radius = radius

In the above code, I added three attributes, one is a class attribute object_type and two of them are instance attributes, center and radius. A circle is a plane object, which is common across all circles. So I created the object_type as a class attribute. On the other hand, each circle has its own center and radius, and I defined them as the instance attributes. Note that the instance attributes have self. This is a convention used to represent the instance of the class itself. By adding self in front of the attribute name, we can allow each instance to have its own value for the attributes (Tip 1).

Tip 1: TIPS: self

The self represents the instance of the class itself.

NOTE

The __init__() method is a special method in “Python” classes that is called when a new instance of the class is created. It is used to initialize the object’s attributes or perform any setup that is necessary. I will explain it later in this article (Section 5.2).

In the next sections, we will check the differences between the class attribute and the instance attribute in detail.

5.1.1 Class Attributes

Class attributes are defined directly inside the class but outside of any methods. They are shared by all instances of the class:

# Create an instance of the Circle class with center coordinates (0, 0) and the radius 1.
circle_1 = Circle([0, 0], 1)
# Create an instance of the Circle class with center coordinates (1, 1) and the radius 2.
circle_2 = Circle([1, 1], 2)

# Print the object_type of circles using the class variable
print(Circle.object_type)
# Print the object_type of circle_1 and circle_2 using the instance variable (will also be 2)
print(circle_1.object_type)
print(circle_2.object_type)
plane
plane
plane

In the above code, I created two instances of Circle class: circle_1 and circle_2. Since object_type is a class attribute, it has a shared value of "plane". You can access the class attribute without instances: Circle.object_type. Class attributes are useful for defining properties that are common to all instances of the class.

5.1.2 Instance Attributes

Instance attributes belong to each instance and can have different values across instances. Since instance attributes belong to instances, you cannot access them directly from the class:

try:
    print(Circle.center)    # Circle class does not have "center".
except AttributeError as e:
    print(e)

print(circle_1.center)     # circle_1 is an instance and has an own value of "center"
print(circle_2.center)     # circle_2 has a different value for its "center" attribute compared to circle_1
type object 'Circle' has no attribute 'center'
[0, 0]
[1, 1]

5.1.3 Conclusion to Instance Attributes

Now you have understood the differences between the class attributes and instance attributes:

TIPS: Class Attributes and Instance Attributes

  • Class Attributes: Variables defined directly inside the class but outside of any methods
    • They are shared by all instances of the class.
    • They can be accessed without instances.
  • Instance Attributes: Variables belong to each instance
    • They can have different values throughout the instances.

5.2 Methods

Methods are functions defined within a class. In “Python” language, the term “method” has a different meaning in the different context, but we will focus on the methods as the functions defined in a class in the article.

First, let’s check the __init__() method we have created in the Circle class.

5.2.1 Constructor Method (__init__())

The __init__() method is a special method in “Python” classes that is called when a new instance of the class is created. It is used to initialize the instance attributes. The syntax for the __init__() method is as follows:

def __init__(self, param1, param2, ...):
    # Initialize attributes here
    self.param1 = param1
    self.param2 = param2
    # ...

As you have learned in Tip 1, the self refers to the instance of the class itself and is used to access the attributes and methods of that instance. When you call the __init__() method, the instance is passed to the method as an argument in “Python” language. So you need self as the parameter of __init__():

class Circle:
    object_type = "plane"

    # Without "self"
    def __init__(center, radius):
        self.center = center
        self.radius = radius

try:
    # This raises TypeError
    circle_1 = Circle([0, 0], 1)
except TypeError as e:
    print("TypeError:", e)

class Circle:
    object_type = "plane"

    # With "self"
    def __init__(self, center, radius):
        self.center = center
        self.radius = radius

# This raises no error
circle_2 = Circle([1, 1], 2)
TypeError: Circle.__init__() takes 2 positional arguments but 3 were given

In the first class, __init__() method is defined without self. At circle_1 = Circle([0, 0], 1), the code tries to pass the following three arguments to __init__():

  • circle_1 (the instance itself)
  • [0, 0] (for center)
  • 1 (for radius)

However, this __init__() method is missing self and has only two parameters: center and radius in the definition. Therefore circle_1 = Circle([0, 0], 1) raises an error.

When you define the __init()__ method with self, on the other hand, raises no error. So remember that you need self when defining __init__() method (See Figure 1).

Figure 1: The self in __init()__ Method

As you will learn in Section 5.2.2, __init__() belongs to the instance methods.

5.2.2 Instance Methods

When you need to work with instance-specific data, you can use instance methods:

import math

class Circle:
    # Class attribute
    object_type = "plane"

    # Constructor method that initializes a Circle object with the coordinates of the center and the radius
    def __init__(self, center, radius):
        # Instance attributes
        self.center = center
        self.radius = radius

    def __str__(self):
        return f"Circle with a center at {self.center} and a radius of {self.radius}"

    # Instance method for calculating the area of the circle
    def area(self):
        return math.pi * self.radius ** 2

circle_1 = Circle([0, 0], 1)
circle_1.area()
3.141592653589793

In the Circle class, I added an instance method area(). To define an instance method, you always need self parameter as you did with __init__() method.

TIPS: The Structure of an Instance Method

class ClassName:
    def instance_method(self, param1, param2, ...):
        # Method definition

Since the area of a circle is specific to each circle, I created the area() method as the instance method. Using instance methods, you can use the instance-specific data (instance attributes).

Note that I also added __str__() instance method to be used later on. Using the __str__() method allows you to control how your objects are represented as strings, making it easier to display meaningful information about your objects when converted to strings or printed:

print(circle_1)

message = str(circle_1)
print(message)
Circle with a center at [0, 0] and a radius of 1
Circle with a center at [0, 0] and a radius of 1

5.2.3 Class Methods

When you need to perform operations that do NOT use data of a specific instance, you can use class methods. You can use a class method without creating a new instance. The structure of a class method is given here:

class ClassName:
    @classmethod
    def class_method(cls, param1, param2, ...):
        # Method definition

To define a class method in “Python” programming, you can use the @classmethod decorator before the method definition. The class method takes cls (the class itself) as the first parameter, similar to self in the instance method.

Now the time to add a class method to our Circle class:

class Circle:
    # Class attribute
    object_type = "plane"

    # Constructor method that initializes a Circle object with the coordinates of the center and the radius
    def __init__(self, center, radius):
        # Instance attributes
        self.center = center
        self.radius = radius

    def __str__(self):
        return f"Circle with a center at {self.center} and a radius of {self.radius}"

    @classmethod
    def from_radius(cls, radius):
        return cls(center=[0, 0], radius=radius)

    # Instance method for calculating the area of the circle
    def area(self):
        return math.pi * self.radius ** 2

circle_1 = Circle.from_radius(1)
circle_2 = Circle.from_radius(2)

print(circle_1)
print(circle_2)
Circle with a center at [0, 0] and a radius of 1
Circle with a center at [0, 0] and a radius of 2

As I explained, from_radius() method is defined after the @classmethod decorator and it takes cls as the first parameter. When you call the method, the Circle class will be assigned to the cls.

It will sometimes be useful to create a circle with a given radius at the origin. Since the from_radius() method takes only radius, you have no need to pass the center coordinates. You need from_radius() method before creating a new instance, so it is defined as a class method (Remember that you can use a class method without an instance).

5.2.4 Conclusion to Instance Methods and Class Methods

You have learned instance methods and class methods and compared them:

Tip 2: TIPS: Instance Methods and Class Methods

Instance Methods:

Table 1: Comparing Instance Methods and Class Methods
Methods For handling instance-specific data First Parameter
Instance Method Yes self
Class Method No cls (with @classmethod decorator)

5.3 Setters and Getters

5.3.1 How to Define Setters and Getters

The Circle class has two attributes, center and radius, that we can edit the values. As you have learned, we can set the values by Circle([0, 0], 1) and get the values by circle_1.center or circle_1.radius. Then, what will happen when we assign negative values to the attributes?

circle_1 = Circle([-1, -1], -2)
print(circle_1)
Circle with a center at [-1, -1] and a radius of -2

Now the circle_1 represents a circle with a center at (-1, -1) and a radius of -2. The coordinates can take negative values, so (-1, -1) is a proper input. However, how about the radius? The radius is a distance between the center and the point on the circumference and it should take a non-negative value.

Therefore, we need a method to check if a correct value is passed to the attributes. The method is called a setter. After setting the proper value, you will need to get the value of the attribute. This is done by a method called a getter.

In “Python” programming, adding the @property decorator makes a method_name method to the getter, and @method_name.setter creates the setter (Figure 2).

Figure 2: Defining a Setter and a Getter
Green box: instance attribute. Blue box: Decorator to define a setter or getter. Red box: Setter or getter name that you can use to access the instance attribute. This is just an example, and there are other ways of defining these methods.

In Figure 2, the __init__() method takes two parameters: self and param1. Inside the __init__() method, param1 is assigned to an instance attribute self.param1. When you write self.param1 = ..., this always calls a setter method. This is useful when you validate the input before assigning it to the attribute.

The @property decorator is used to define a method that works as a getter, in this case, param1. The param1 method will return the value of the self.param1 attribute.

By using the same name as the param1 getter, @param1.setter decorator defines a method to set the value of the self.param1 attribute. The setter will set the value of self.param1 when called.

This is an example of defining a setter and a getter, and there are other ways to do it. I recommend using the name of param1 in red boxes and using _param1 (with an underscore) in green boxes.

6 NOTE

The _ is used to indicate that the attribute is accessed only in the class. In other words, we will NOT access the value of _param1 by class_instance._param1 from outside of the class.

I summarize the setters and getters.

TIPS: How to Create a Setter and a Getter

  • Getter:
    • Use @property decorator
  • Setter:
    • Use @getter_name.setter

Getters and setters are methods but work like attributes. They are essentially methods, so you can use additional logic, to check the validity of the input value.

6.0.1 Adding Setters and Getters to the Circle Class

Now, we will add the setters and getters to the Circle class and modify some of the codes:

class Circle:
    object_type = "plane"

    def __init__(self, center, radius):
        # Instance attributes
        # No need to define self._center or self._radius here when using property.
        # This is for the readability.
        self.center = self._center = center
        self.radius = self._radius = radius

    def __str__(self):
        return f"Circle with a center at {self._center} and a radius of {self._radius}"

    # Getter for _radius
    @property 
    def radius(self):
        return self._radius

    # Setter for _radius
    @radius.setter
    def radius(self, value):
        if not value < 0:
            self._radius = value
        else:
            raise ValueError("Radius must be non-negative")

    # Getter for _center
    @property
    def center(self):
        return self._center

    # Setter for _center
    @center.setter
    def center(self, value):
        self._center = value

    @classmethod
    def from_radius(cls, radius):
        return cls(center=[0, 0], radius=radius)

    def area(self):
        return math.pi * self._radius ** 2

circle_1 = Circle([0, 0], 1)
circle_2 = Circle([1, 1], 2)
print("# Initialization")
print(circle_1)
print(circle_2)

# Set a radius of 4 using the setter
circle_1.radius = 4
print("n# Edited the circle_1 radius")
print(circle_1)
print(circle_2)

# Get a center using the getter
print("n# Getting the center and radius")
print(circle_1.center)
print(circle_1.radius)

# -3 is not a valid input for the radius, and the setter raises ValueError.
try:
    circle_1.radius = -3
except ValueError as e:
    print("n# Error in Setter")
    print(e)

try:
    # Setters also work when initializing the instance
    circle_3 = Circle([0, 0], -3)
except ValueError as e:
    print("n# Error in Constructor")
    print(e)

try:
    # This also triggers constructor and raises the error
    circle_4 = Circle.from_radius(-3)
except ValueError as e:
    print("n# Error in from_radius")
    print(e)

# Avoid directly referencing attributes prefixed with an underscore.
print(circle_1._radius)
circle_1._radius = -3
print(circle_1)
# Initialization
Circle with a center at [0, 0] and a radius of 1
Circle with a center at [1, 1] and a radius of 2

# Edited the circle_1 radius
Circle with a center at [0, 0] and a radius of 4
Circle with a center at [1, 1] and a radius of 2

# Getting the center and radius
[0, 0]
4

# Error in Setter
Radius must be non-negative

# Error in Constructor
Radius must be non-negative

# Error in from_radius
Radius must be non-negative
4
Circle with a center at [0, 0] and a radius of -3

In the __init__() method, the input values are assigned to the instance attributes. When you write self.center = ..., the setter method is called, and it checks the validity of the input.

The __init__() method assigns the input to both self._center and self._radius. As I mentioned, the underscore, _, before the attribute name implies that the attribute is accessed only in the class. These attributes will be used in the setter, getter, or other places in the class.

In Figure 2, there is no self._param1 = param1 in the __init__() method. So in the Circle class, we can rewrite the code like:

  • self.center = self._center = center rightarrow self.center = center
  • self.radius = self._radius = radius rightarrow self.radius = radius

Nevertheless, for the readability, I defined both self._center and self._radius in the __init__() method. This is a confusing topic, and you do not need to care about it for now. Just remember that this __init__() method can use the setter to validate the input and may improve some readability.

Then, I added the setters and getters for the instances. You will notice that the setter for the radius has a logic to check if the non-negative value is passed to the instance. In the above code, I tried to pass -3 to the setter. Negative values are not proper for the radius, so the setter raised ValueError.

In a nutshell, by using setters, you can add logic to check the input value, and the getters allow you to get the values of attributes.

NOTE

If you run circle_1._radius, you can directly access the instance attribute without the getter. Also, circle_1._radius = -3 can assign the negative value to the instance attribute even if there is the setter to check the input value.

This is how the programming language works. With this in mind, you can simply write circle_1.radius or circle_1.radius = -3 so that the code uses the setters and getters.

6.0.2 Conclusion to Setters and Getters

You have learned the setters and getters, and it is the time to review the concepts:

TIPS: Setters and Getters

Table 2: Comparing Setters and Getters
Method How to Define Purpose
Setter Use @getter_name.setter Validating and setting the input
Getter Use @property decorator Getting the value of the attribute

After checking the above basic knowledge of setters and getters, you review the difference between radius and _radius.

class Circle:
    def __init__(self, center, radius):
        self.radius = self._radius = radius

    # Getter for _radius
    @property 
    def radius(self):
        return self._radius

    # Setter for _radius
    @radius.setter
    def radius(self, value):
        if not value < 0:
            self._radius = value
        else:
            raise ValueError("Radius must be positive")

circle_1 = Circle([0, 0], 1)
circle_1.radius = 2

The underscore, _, before the attribute name implies that the attribute is accessed only in the class. You don’t write circle_1._radius = 2 to set the value from outside of the class or the instance. You should use circle_1.radius = 2. This way allows you to run the setter before assigning the input to the attribute.

In the class, you will use self._radius instead of self.radius because the value stored in self._radius has been validated by the setter and you do not need to run the setter again.

7 Exercise

Exercise 1  

# Create a class "Rectangle" following the instructions:
# 1. The class has a class attribute "object_type". The value is ""plane"".
# 2. The class instance can be initiated with two parameters: "width" and "height".
    # 2-1. The "width" and "height" should take non-negative values.
    # 2-2. The "width" and "height" should be validated by the setters in the __init__() method.
# 3. The __init__() method has the following instance attributes:
    # - "self.width" and "self._width"
    # - "self.height" and "self._height"
# 4. The class has setters and getters: "width()" and "height()"
    # 3-1. The getters simply return "self._width" or "self._height"
    # 3-2. The setters validate that the input values are non-negative.
    # 3-3. If the input is non-negative, the setters set the inputs to "self._width" or "self._height".
    # 3-3. Otherwise, they raise a "ValueError" with the message "Width must be non-negative" or "Height must be non-negative".
# 5. The class has a class method "from_width()". This method takes the length of width as the input. So a square will be created.
# 6. The class has an instance method "area()". This returns to the area of the rectangle.

8 Conclusion

You have learned a lot about setters and getters in “Python” programming. There are other important concepts related to classes, such as inheritance and polymorphism. However, you have gained the knowledge and skills to delve into these advanced topics in the future.