Python Class Definition: Object Oriented Programming Made Easy

Understanding how to work on the definition of a Python Class is the first step to move from procedural programming to object oriented programming.

Creating the definition of a Python class means writing code that allows you to put together the data and behaviours that best describe a specific concept or entity. The data in a class is represented by attributes and the behaviours of the class are given by methods.

In this tutorial we will create together a class part of a bigger application that provides weather forecasts. We will define the class that represents a city, including its attributes and methods.

Let’s get started!

A Basic Python Class Example

Firstly, as part of our weather forecasts application, we want to create a class that represents a city.

In this specific context, a city can have different attributes. We will use the following:

  • temperature
  • weather type
  • precipitation chance (percentage)
  • humidity (percentage)
  • wind speed (km/h)

At the same time we want the city class to have a method that provides the weather summary for that city.

We will start from the class attributes, the first step is to define a constructor:

class City:
  
    def __init__(self, temperature, weather_type, precipitation_chance, humidity, wind_speed):
        self.temperature = temperature
        self.weather_type = weather_type
        self.precipitation_chance = precipitation_chance
        self.humidity = humidity
        self.wind_speed = wind_speed

The name of the constructor method for a class in Python is __init__. The first parameter taken by this method is self which represents an instance of this class also known as an object.

Let’s clarify this concept before we continue…

A class is a template or blueprint that allows to create objects (or class instances). For example, we can use the class City to create two objects, one for London and one for Paris forecasts.

Each object can have different values for its attributes and it’s stored in a different area in memory.

As mentioned before…

The self parameter is the first parameter in class methods and it refers to an instance of a class.

Let’s see how we can create an object of type City:

london = City(21, 'Sunny', 0.1, 0.63, 10)
print(london)
print(london.__dict__)

The first line creates an object called London, when we create an object the __init__ method of the City class is called automatically.

As you can see we are passing five arguments when building the object.

But, this doesn’t match with the number of parameters accepted by the __init__ method that is six. This is because the self parameter doesn’t need to be specified when building an instance of a class, it automatically refers to it.

The output of the first print statement is:

<__main__.City object at 0x109755a10>

It shows the reference to an object of type City and its location in memory.

The output of the second print statement is:

{'temperature': 21, 'weather_type': 'Sunny', 'precipitation_chance': 0.1, 'humidity': 0.63, 'wind_speed': 10}

The __dict__ method prints the namespace of an object (as you can see it’s a Python dictionary). In this case we can see the value of the attributes that we have set using the constructor of the City class.

We will have a look more at namespaces later in this tutorial. Looking at the values in the namespace of an object can be also very useful when learning about inheritance.

Adding a Method to Our Python Class

In this section, now that we have defined the attributes for our class, I want to create a method that prints the weather summary, I will call it print_summary.

Here is the method:

def print_summary(self):
        print('Weather forecasts for London - {}\nTemperature: {}°\nChance of precipitation: {}%\nHumidity: {}%\nWind speed: {} km/h\n'
            .format(self.weather_type, self.temperature, int(self.precipitation_chance*100), int(self.humidity*100), self.wind_speed))

As you can see the only parameter for this method is self. As we have explained before, self is used to pass the instance of our class to the method.

We are basically printing the object attributes in a readable format.

After creating our city object we can call this method with the dot notation:

london = City(21, 'Sunny', 0.1, 0.63, 10)
london.print_summary()

And the ouput is:

Weather forecasts for London - Sunny
Temperature: 21°
Chance of precipitation: 10%
Humidity: 63%
Wind speed: 10 km/h

You can see how this method helps us create a standard way to display weather forecasts for a given city. If this method wasn’t there different developers using our City class would display this information in a different way.

Understanding Instance Attributes

The five attributes set inside the __init__ method of our class are called instance attributes (or instance variables) because they belong to a specific instance of our class.

Let’s create another object based on the City class, this time for Rome.

The first thing I notice is that our class doesn’t have an attribute for the name of the city, so let’s add that to the constructor.

Our class becomes:

class City:
  
    def __init__(self, city_name, temperature, weather_type, precipitation_chance, humidity, wind_speed):
        self.city_name = city_name
        self.temperature = temperature
        self.weather_type = weather_type
        self.precipitation_chance = precipitation_chance
        self.humidity = humidity
        self.wind_speed = wind_speed

    def print_summary(self):
        print('Weather forecasts for {} - {}\nTemperature: {}°\nChance of precipitation: {}%\nHumidity: {}%\nWind speed: {} km/h\n'
            .format(self.city_name, self.weather_type, self.temperature, int(self.precipitation_chance*100), int(self.humidity*100), self.wind_speed))

I have added a new parameter to the constructor, called city_name, then set the value of the instance attribute city_name.

Also, I have updated the print_summary method to use the name of the city from our new instance attribute.

Considering that the number of parameters the __init__ method needs has changed, we will also have to change the way we create our object, we also need to pass a string for the name of the city as first argument.

london = City('London', 21, 'Sunny', 0.1, 0.63, 10)
london.print_summary()

rome = City('Rome', 32, 'Sunny', 0.05, 0.67, 5)
rome.print_summary() 

And here is the output we get from the print_summary class method:

Weather forecasts for London - Sunny
Temperature: 21°
Chance of precipitation: 10%
Humidity: 63%
Wind speed: 10 km/h

Weather forecasts for Rome - Sunny
Temperature: 32°
Chance of precipitation: 5%
Humidity: 67%
Wind speed: 5 km/h

Let’s take one of the instance attributes, for example the temperature, and print its value for both objects:

print(london.temperature)
print(rome.temperature)
21
32

As you can see the value of the instance attribute temperature changes between our two instances of the City class.

And I can do the following to update this instance attribute for the london object:

london.temperature = 23
london.print_summary()

The output is now:

Weather forecasts for London - Sunny
Temperature: 23°
Chance of precipitation: 10%
Humidity: 63%
Wind speed: 10 km/h

Now, let’s have a look at a different type of attribute that we can have in a class.

Declaring Class Attributes in Python

There are cases in which it makes sense to define attributes at class level instead of attributes at instance level. This is what we define class attributes (we can also call them class variables considering that attributes are variables).

You can read and update a class attribute using the dot notation with the class name before the dot instead of using the name of the instance.

Here is an example, I want to add a class attribute to our City class. Our class attribute is a list that contains valid weather types. The aim is to be able to validate the weather_type passed to the constructor when we create a new object.

To define a class attribute we do the following (everything else in our class remains unchanged for now):

class City:
  
    valid_weather_types = ['Sunny', 'Cloudy', 'Rainy']

The instances of a class have access to a class attribute and the same applies to the class itself.

Here is what I mean:

print(london.valid_weather_types)
print(rome.valid_weather_types)
print(City.valid_weather_types)

['Sunny', 'Cloudy', 'Rainy']
['Sunny', 'Cloudy', 'Rainy']
['Sunny', 'Cloudy', 'Rainy']

We want to make sure our program raises an exception if an incorrect weather type is passed via the __init__ constructor. Our constructor becomes:

def __init__(self, city_name, temperature, weather_type, precipitation_chance, humidity, wind_speed):
        if weather_type not in City.valid_weather_types:
            raise ValueError('Invalid weather type provided.')
        self.city_name = city_name
        self.temperature = temperature
        self.weather_type = weather_type
        self.precipitation_chance = precipitation_chance
        self.humidity = humidity
        self.wind_speed = wind_speed

As you can see we raise a ValueError exception if the user passes an invalid weather type while creating a new object.

Let’s create an object with an incorrect weather type and see what happens:

athens = City('Athens', 34, 'Partly cloudy', 0.02, 0.81, 4)

Here is the error we get back:

Traceback (most recent call last):
  File "city.py", line 25, in <module>
    athens = City('Athens', 34, 'Partly cloudy', 0.02, 0.81, 4)
  File "city.py", line 7, in __init__
    raise ValueError('Invalid weather type provided.')
ValueError: Invalid weather type provided.

As expected we get a ValueError back due to the invalid weather type.

However, this is just an example of how the constructor can validate instance attributes. It doesn’t prevent us from setting the value of weather_type to an incorrect value outside of the constructor.

This is what I mean:

athens = City('Athens', 34, 'Cloudy', 0.02, 0.81, 4)
athens.weather_type = 'Partly cloudy'
print(athens.weather_type)

Partly cloudy

I have created the athens object using a valid weather_type and then set its value to an invalid weather_type outside of the constructor without receiving any errors back.

I will show you how we can do better validation in a different tutorial. For now, the goal is to start getting familiar with Python classes.

Python Class and Instance Namespaces

To understand how Class and Instance attributes work, it’s important to introduce the concept of namespace applied to a Python class.

A namespace is a way to create a mapping between names and objects in Python and it’s implemented using the dictionary data structure.

We will have a look at the namespaces for the class City and for the class instance london to see the difference between the two.

To print the content of each namespace we use the dot notation followed by __dict__.

Here is the namespace for the City class:

print(City.__dict__)

{'__module__': '__main__', 'valid_weather_types': ['Sunny', 'Cloudy', 'Rainy'], '__init__': <function City.__init__ at 0x105b01710>, 'print_summary': <function City.print_summary at 0x105b0eb90>, '__dict__': <attribute '__dict__' of 'City' objects>, '__weakref__': <attribute '__weakref__' of 'City' objects>, '__doc__': None}

And here is the namespace for the class instance london:

print(london.__dict__)

{'city_name': 'London', 'temperature': 21, 'weather_type': 'Sunny', 'precipitation_chance': 0.1, 'humidity': 0.63, 'wind_speed': 10}

So, we can see that the class attribute valid_weather_types is in the class namespace and it’s not in the instance namespace. The instance namespace only contains attributes set in the class constructor.

So, how can I do the following if the instance namespace doesn’t contain the valid_weather_types attribute?

print(london.valid_weather_types)

['Sunny', 'Cloudy', 'Rainy']

Here is the logic that Python follows:

  • Look for the attribute in the instance namespace.
  • If the attribute is not in the instance namespace then look in the class namespace (that’s where Python finds the valid_weather_types class attribute).

Now, let’s say we do the following:

london.valid_weather_types = ['Sunny', 'Cloudy', 'Partly cloudy', 'Rainy']

Are we changing the class attribute we have defined before?

Let’s have a look at the namespaces again:

print(City.__dict__)
{'__module__': '__main__', 'valid_weather_types': ['Sunny', 'Cloudy', 'Rainy'], '__init__': <function City.__init__ at 0x10c773710>, 'print_summary': <function City.print_summary at 0x10c780b90>, '__dict__': <attribute '__dict__' of 'City' objects>, '__weakref__': <attribute '__weakref__' of 'City' objects>, '__doc__': None}

print(london.__dict__)
{'city_name': 'London', 'temperature': 21, 'weather_type': 'Sunny', 'precipitation_chance': 0.1, 'humidity': 0.63, 'wind_speed': 10, 'valid_weather_types': ['Sunny', 'Cloudy', 'Partly cloudy', 'Rainy']}

Based on the output above, the value of valid_weather_types in the class namespace hasn’t changed. At the same time, we can see that a valid_weather_types attribute has been added to the instance namespace with the new value we have provided.

So our assignment has added a new instance attribute to the london instance.

This new attribute is not visible to other instances that continue referring to the attribute set at class level.

Let’s confirm that this is the case for the rome instance:

print(rome.__dict__)
{'city_name': 'Rome', 'temperature': 32, 'weather_type': 'Sunny', 'precipitation_chance': 0.05, 'humidity': 0.67, 'wind_speed': 5}

print(rome.valid_weather_types)
['Sunny', 'Cloudy', 'Rainy']

As expected, the valid_weather_types attribute is not present in the rome instance namespace and it’s resolved via the class namespace.

How to Use Class Attributes

There are different ways in which class attributes can be useful. You can apply this example to many types of applications.

We want to track the number of cities for which we are providing weather forecasts. To store this information we can use a class attribute that we increment every time we create a new class instance.

Let’s define a class attribute called cities_number. We will increase this attribute every time we create a new instance. This means we also have to change the implementation of the __init__ constructor.

Our class becomes:

class City:
  
    cities_number = 0
    valid_weather_types = ['Sunny', 'Cloudy', 'Rainy']

    def __init__(self, city_name, temperature, weather_type, precipitation_chance, humidity, wind_speed):
        if weather_type not in City.valid_weather_types:
            raise ValueError('Invalid weather type provided.')
        self.city_name = city_name
        self.temperature = temperature
        self.weather_type = weather_type
        self.precipitation_chance = precipitation_chance
        self.humidity = humidity
        self.wind_speed = wind_speed
        City.cities_number += 1

[the print_summary method doesn't change]

Now, let’s see what happens to the value of the class attribute cities_number after the creation of two instances:

london = City('London', 21, 'Sunny', 0.1, 0.63, 10)
print(City.cities_number)
1

rome = City('Rome', 32, 'Sunny', 0.05, 0.67, 5)
print(City.cities_number)
2

As expected the value of cities_number is incremented every time a new class instance is created.

Before completing this tutorial we will have a look at another way class attributes can be used for.

Defining a Constant Using a Class Attribute

Class attributes can be used to define constants that are referenced by instance methods for specific calculations.

In our example I want to be able to provide the temperature in Celsius and Fahrenheit. To do that I need a way to convert the Celsius value provided via the constructor into its Fahrenheit representation.

The formula to convert the Celsius temperature (Tc) into Fahrenheit (Tf) is the following:

Tf = Tc * 9/5 + 32

In our class let’s define 9/5 (that is equal to 1.8) as a class attribute called temperature_conversion_factor.

We will then use this class attribute to print the Fahrenheit temperature using the print_summary method. Below you can see the updated class:

class City:
    cities_number = 0
    valid_weather_types = ['Sunny', 'Cloudy', 'Rainy']
    temperature_conversion_factor = 1.8

    def __init__(self, city_name, temperature, weather_type, precipitation_chance, humidity, wind_speed):
        if weather_type not in City.valid_weather_types:
            raise ValueError('Invalid weather type provided.')
        self.city_name = city_name
        self.temperature = temperature
        self.weather_type = weather_type
        self.precipitation_chance = precipitation_chance
        self.humidity = humidity
        self.wind_speed = wind_speed
        City.cities_number += 1

    def print_summary(self):
        print('Weather forecasts for {} - {}\nTemperature: {}°C / {}°F\nChance of precipitation: {}%\nHumidity: {}%\nWind speed: {} km/h\n'
            .format(self.city_name, self.weather_type, self.temperature, int(self.temperature*City.temperature_conversion_factor+32), int(self.precipitation_chance*100), int(self.humidity*100), self.wind_speed))

Let’s have a look at the part of the code that converts the temperature from Celsius to Fahrenheit:

int(self.temperature*City.temperature_conversion_factor+32)

The thing to highlight is that in this formula I’m using the class attribute temperature_conversion_factor.

Considering that it’s a class attribute, to refer to it I’m using the class name (City) followed by a dot and by the name of the attribute.

If I call the print_summary method for the london and rome objects I get the following output:

Weather forecasts for London - Sunny
Temperature: 21°C / 69°F
Chance of precipitation: 10%
Humidity: 63%
Wind speed: 10 km/h

Weather forecasts for Rome - Sunny
Temperature: 32°C / 89°F
Chance of precipitation: 5%
Humidity: 67%
Wind speed: 5 km/h

And this is it for this tutorial!

Here is a video recap to help you retain more info about creating a class in Python:

Conclusion

In conclusion, in this tutorial, we have worked on the definition of a Python class. We started with the class __init__ method that works as constructor and it allows to create new class instances (or objects).

Inside the constructor we have set instance attributes based on the values passed by the user when creating an object of type City.

Then we have defined a method that prints the summary of the weather forecasts for a specific city. Very useful to standardise the way we show data to our users.

The next step has been understanding the difference between instance attributes and class attributes. We have also seen the role of namespaces in the way class and instance attributes are identified in a Python program.

Finally, we have seen how to use class attributes to:

  • track a metric at class level (e.g. the number of cities for which we provide weather forecasts).
  • define a constant you can use in the methods of our class.

You now have enough knowledge to start creating your own classes!

Let me know if you have any questions 🙂

If you want to keep building your object oriented programming knowledge have a look at these articles about Python inheritance and Python abstract classes.

Leave a Comment