Advance Your Python Skills
Attributes in Python — 6 Concepts to Know
Better understand Python as a unique OOP language.
In the modern programming world, object-oriented programming (OOP) languages have played an evolutionary role in changing the design and implementation patterns in software development. As a critical member of the OOP family, Python has gradually gained popularity in the last 10 years or so. Like other OOP languages, Python operates its data around a vast diversity of objects, including modules, classes, and functions.
If you have any coding experience in any OOP language, you should probably know that all objects have their internal characteristics data called fields, attributes, or properties. In Python, these object-bound characteristics data are commonly known as attributes. In this article, I would like to talk about them, specifically in the context of a custom class.
1. Class Attributes
To better manage data in our projects, we often need to create custom classes. In Python, classes are objects too, which means that they can have their own attributes. Let’s see an example.
As shown above, we declared a class called
Dog. Because all dogs belong to the Canis genus and Canidae family (Wikipedia), we created two class attributes named
family to store these two pieces of information. As you can see, we could directly use the class to access these attributes. We can use the introspection function
dir to show the list of the
Dog’s attributes, which include family and genus.
These attributes that are defined as the class level are known as class attributes, which are directly retrievable by the class. However, unlike some other OOP languages, instance objects in Python can access these class attributes directly too, as shown in the below code snippet.
2. Instance Attributes
With custom classes, we can also set attributes to instance objects. These attributes are known as instance attributes, which means that they are instance-specific data. Let’s continue with the
In the above code, we defined the
__init__ function, which will serve as the construction method to create a new
Dog instance. The first argument
self refers to the instance that we’re creating. During the instantiation (i.e., creating the new instance), we will assign the breed and name information to the new instance object, and these attributes will become part of the instance’s characteristics, as shown below.
One thing to note is that we can assign a value to an instance with the same attribute as the class attribute. In this case, when you retrieve this attribute of the instance, the class attribute won’t be retrieved. In other words, when you use an instance object to retrieve the class attribute, Python will first check if the instance itself has an attribute that has been set with the same name. If none, Python will use the class attribute as the fallback. In addition, setting an instance’s attribute won’t affect the class’s attribute of the same name. Let’s see these characteristics in the following code snippet.
3. Functions As Attributes
In Python, everything is an object, and previously, I already mentioned that classes are objects. Moreover, functions are Python objects. Within a class, we can define functions, which are commonly referred to as methods. Depending on how the functions are used, we can further categorize them as class methods, static methods, and instance methods. It’s not essential here to understand these differences (see my previous articles: this and that, if you want to learn them).
Although some OOP languages treat attributes (or properties) and functions as different entities, Python considers these methods (functions) as attributes of the class — not very different as the class attributes that we defined earlier. Let’s just update the
Dog class with all three kinds of methods as mentioned above: class, static, and instance methods, as shown below.
With the updated class, we can use the introspection function
dir to check the list of attributes of the class. As shown below, both class and static methods are included in the list.
However, one thing that may be surprising to some people is the inclusion of the instance method
bark in the list. As we know, instance methods are those functions that are called by the instance objects, and thus some people may think that these instance methods should be bound to all individual instances. However, it’s not the case in Python. Let’s see the following code before I explain how an instance method works.
As shown above, we first created an instance of the
Dog class. Like other OOP languages, the instance object could directly invoke the instance method
bark. However, what Python does differently from other languages is that the invocation of the instance method is operated through the class calling the defined function by passing the instance as an argument (i.e.,
Dog.bark(dog)). In other words,
instance.inst_method() is essentially the same as
Class.inst_method(instance) in Python.
It is made possible because it is the
Dog class that “owns” the instance method, which is a mechanism that saves memory as Python doesn’t need to create individual function copies for each instance object. Instead, whenever an instance calls an instance method, Python delegates the call to the class, which would call the corresponding function by passing the instance (it is to be set to the
self argument in the defined function).
4. Private Attributes
If you have any experience in OOP, you shouldn’t be unfamiliar with the existence of access modifiers, such as public, private, and protected. These modifiers restrict the scope of where the modified attributes and functions can be accessed. However, you rarely hear such a discussion in Python. Practically, all Python attributes are public, if we borrow the terminology in the OOP world. As shown above, the class and instance attributes are all freely accessible where these classes and instances are accessible. Thus, strictly speaking, there are no real private or protected attributes (to be discussed next) in Python. We’re just using these terms analogously, such that it’ll be easier for programmers coming from other OOP background to appreciate related coding conventions (yes, just a convention, not reinforced as real access control).
Let’s first talk about how we can define a “private” attribute in Python. The convention is to name those attributes with two leading underscores and no more than one trailing underscore. Consider the following example of the updated
Dog class — for simplicity, we omit other attributes that we defined previously.
After the above update, the
Dog instances will have a private attribute called
tag, as indicated by its name. The instance object could still access its other attributes (e.g.,
name) as before. However, the instance couldn’t access the private attribute
__tag as we may be expecting. Indeed, such a restriction of accessing these attributes is exactly how they can be called “private” attributes. But how did it happen, under the hood? After all, I previously mentioned that all Python attributes are public by default. The following shows you how Python implements “private” attributes.
__dict__ special method (also known as a dunder method, those with double underscores before and after the name) is able to show a dictionary representation of the object. Specifically, the key-value pairs in the dictionary are the attributes and their values of the object. As we can see, besides the
name attributes, there is another attribute called
_Dog__tag. This attribute is exactly how the private attribute
__tag is associated with the object through a procedure called mangling.
Specifically, mangling or name mangling is to use
_ClassName as the prefix for the private attributes, such that we artificially create an access restriction to these “private” attributes. However, if we do want to retrieve any private attribute, we can still access it using the mangled name, like what we did in the code snippet using
5. Protected Attributes
In the last section, we talked about private attributes, but how about protected attributes? The counterparts of protected attributes in Python are attributes whose names have only one underscore. Unlike the double underscores which will result in mangling, the single underscore prefix doesn’t change how Python interpreter handles these attributes — It’s merely a convention in the Python programming world to denote that they (i.e., the coder) doesn’t want you to access these attributes. However, again, if you insist on accessing them, you can still do that. Let’s see the code below.
We updated the class
Dog by creating an instance attribute called
_nickname. As indicated by its name using an underscore prefix, it is considered as a “protected” attribute by convention. We can still access these protected attributes as other “public” attributes, but some IDEs or Python editors will not provide prompts (e.g., autocompletion hints) for these non-public attributes. See the screenshot for these examples using the Jupyter Notebook.
If we work with modules instead of classes as we’re doing here, when we import the module using
from the_module import *, the names with the underscore prefix won’t be imported, providing a mechanism for restricting access to these “protected” attributes.
Some OOP languages use attributes and properties interchangeably, but they are different concepts in Python. As discussed, attributes in Python are a broad concept that can include different kinds of attributes and functions. However, properties in Python are essentially functions that are decorated with the
@property decorator. The decorated function will provide the callers with easier access to these properties. They can often be used together with the protected attributes. Let’s see an example.
In the above code, we defined a function called
nickname, which was decorated by the
@property decorator. With the decoration, we can access the
nickname property using the dot notation, just like accessing regular attributes, as shown below.
However, unlike regular attributes, we couldn’t set these properties, as shown above. In other words, using the
@property decorator together with protected attributes provides a mechanism to implement a “read-only” attribute. Notably, it’s possible to set the property if we choose to do so — with the following update.
As shown in the above code snippet, we defined a setter method for the property nickname by using another property-related decorator function. With this change, we can now set the property as a regular attribute using the dot notation, as shown below. There are additional functions to fully configure the behaviors of a property (e.g., deleter), and you can find out these in my earlier article on the topic.
In this article, we reviewed 6 important concepts related to attributes in Python, particularly in the context of custom classes. Understanding these concepts can help you better master Python as a unique OOP language.
Thanks for reading this piece and happy coding.