Python
10 Remarkable Python OOP Tips That Will Optimize Your Code Significantly
Make your Python classes maintainable and extendable

Object-oriented programming (OOP) is a great programming paradigm to create modular and reusable code that is easy to maintain and extend.
Python, as an excellent programming language, provides full OOP functionalities to us, including some unique features that even pure OOP languages don’t have.
However, mastering OOP is not as easy as understanding some basic syntax and tricks.
This article will summarise 10 advanced techniques for writing better OOP programs in Python.
After reading and practising, you will stand out in the long run of software engineering in Python. 👍
1. Use Data Classes To Automatically Generate Special Methods
To understand the data classes of Python, let’s start with a simple example.
The following code defines a class named Point representing points in Euclidean space:
class Point:
def __init__(self,x,y):
self.x = x
self.y = y
A=Point(2,3)
B=Point(2,3)
print(A==B)
# FalseUnfortunately, it printed False even if the two points have the exact same location.
The reason is simple, we didn’t tell Python how to compare different Point instances when defining this class.
Therefore, we have to define the __eq__ method, which will be used to determine if two instances are equal or not:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
A=Point(2,3)
B=Point(2,3)
print(A==B)
# TrueThe above code works as expected. However, it’s too much for just an obvious comparison.
Is there any chance that Python can become more intelligent and define the basic internal methods in advance for us? 🤔
Yes. Since Python 3.7, there is a new built-in decorator — @dataclass. We can define a data class as follows:
from dataclasses import dataclass
@dataclass
class Point:
x:int
y:int
A=Point(2,3)
B=Point(2,3)
print(A==B)
# TrueAs the above code shows, this class definition creates a Point class with two fields, x and y, and their type hints are both int.
We only defined two attributes of the Point class, nothing else. But why Python knows how to compare the points A and B properly this time?
In fact, the @dataclass decorator automatically generated several methods for the Point class, such as __init__ for initializing objects, __repr__ for generating string representations of objects, and __eq__ for comparing objects for equality.
Since the @dataclass decorator simplifies the process of creating data classes by automatically generating many special methods for us, it saves our time and effort in writing these methods and helps ensure that our data classes have consistent and predictable behavior.
Anytime you need to define classes that are primarily used to store data, don’t forget to leverage the power of the @dataclass decorator.
2. Use Abstract Classes To Define Common Interfaces
An abstract class, which is an important concept of OOP, can define a common interface for a set of subclasses. It provides common attributes and methods for all subclasses to reduce code duplication. It also enforces subclasses to implement abstract methods to avoid inconsistencies.
Python, like other OOP languages, supports the usage of abstract classes.
The following example shows how to define a class as an abstract class by abc.ABC and define a method as an abstract method by abc.abstractmethod:
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def move(self):
print('Animal moves')
class Cat(Animal):
def move(self):
super().move()
print('Cat moves')
c = Cat()
c.move()
# Animal moves
# Cat movesThis example defines an abstract class called Animal, and a class Cat which is inherited from Animal.
Given that the Animal is an abstract class and its move() method is an abstract method, we must implement the move() method in the Cat class. This mechanism helps to ensure that all subclasses have a certain set of methods, and helps to prevent errors that might occur if the subclasses do not implement all of the required methods.
The ABC, by the way, is the abbreviation of abstract base class.
3. Separate Class-Level and Instance-Level Attributes
Python classes can be clearly separated into class-level and instance-level attributes:
- A class attribute belongs to a class rather than a particular instance. All instances of this class can access it and it is defined outside the constructor function of the class.
- An instance attribute, which is defined inside the constructor function, belongs to a particular instance. It’s only accessible in this certain instance rather than the class. If we call an instance attribute by the class, there will be an
AttributeError.
class MyClass(object):
class_attr = 0
def __init__(self, instance_attr):
self.instance_attr = instance_attr
MyClass.class_attr
# 0
MyClass.instace_attr
# AttributeError: type object 'MyClass' has no attribute 'instace_attr'
my_instance = MyClass(1)
my_instance.instance_attr
# 1
my_instance.class_attr
# 0The above example shows the different usages of class attributes and instance attributes. Separating these two types of attributes clearly can make your Python code more robust.
4. Separate Public, Protected and Private Attributes
Unlike C++ or Java, Python doesn’t have strict restrictions for the permissions of attributes.
The Pythonic way to separate different permissions is to use underscores:
class Student:
def __init__(self, name, age, grade):
self.name = name # public
self._age = age # protected
self.__grade = grade # privateAs the above code shows, we can define a protected attribute with a single leading underscore. This is just a convention. We can still use it as a public member. But we should not do this. Following good programming conventions will make our code more elegant and readable.
We define a private attribute with double-leading underscores. This mechanism is beyond convention. Python uses name mangling technique to ensure we won’t use a private member inappropriately.
5. Define Mixin Classes through Multiple Inheritance
In Python, a mixin is a class that is designed to add a specific behavior or set of behaviors to one or more other classes. It can provide a flexible way to add functionality to a class without modifying the class directly or making the inheritance relationship of subclasses complicated.
For example, we define a class ToDictMixin as follows:
class ToDictMixin:
def to_dict(self):
return {key: value for key, value in self.__dict__.items()}Now, any other classes that need the converting to dictionary functionality can inherit this mixin class besides its original parent class:
class MyClass(ToDictMixin, BaseClass):
passPython allows multiple inheritances. This is why we can use mixins. But here is a frequently asked question:
Under multiple inheritances, if two parent classes have the same methods or attributes, what will happen?
In fact, if two parent classes have the same method or attribute, the method or attribute in the class that appears first in the inheritance list will take precedence. This means that if you try to access the method or attribute, the version from the first class in the inheritance list will be used.
6. Use @property Decorator To Control Attributes Precisely
In Python, you can access and modify the attributes of an object directly, using dot notation.
However, it is generally a good object-oriented programming practice to access and modify the attributes of an object through their getters, setters, and deleters, rather than directly using dot notation. This is because using getters, setters, and deleters can give you more control over how the attributes are accessed and modified, and can make your code more readable and easier to understand.
For example, the following example defines a setter method for the attribute _score to limit the range of its value:
class Student:
def __init__(self):
self._score = 0
def set_score(self, s):
if 0 <= s <= 100:
self._score = s
else:
raise ValueError('The score must be between 0 ~ 100!')
Yang = Student()
Yang.set_score(100)
print(Yang._score)
# 100It works as expected. However, the above implementation seems not elegant enough.
It would be better if we can modify the attribute like a normal attribute using dot notation but still has the limitations, rather than having to call the setter method like a function.
This is why Python provides a built-in decorator named @propery. Using it, we can modify attributes using dot notation directly. It will improve the readability and elegance of our code.
Now, let’s change the previous program a bit:
class Student:
def __init__(self):
self._score = 0
@property
def score(self):
return self._score
@score.setter
def score(self, s):
if 0 <= s <= 100:
self._score = s
else:
raise ValueError('The score must be between 0 ~ 100!')
@score.deleter
def score(self):
del self._score
Yang = Student()
Yang.score=99
print(Yang.score)
# 99
Yang.score = 999
# ValueError: The score must be between 0 ~ 100!7. Use Class Methods in Classes
Methods in a Python class can be instance-level or class-level, similar to attributes.
An instance method is a method that is bound to an instance of a class. It can access and modify the instance data. An instance method is called on an instance of the class, and it can access the instance data through the self parameter.
A class method is a method that is bound to the class and not the instance of the class. It can’t modify the instance data. A class method is called on the class itself, and it receives the class as the first parameter, which is conventionally named cls.
Defining a class method is very convenient in Python. We can just add a built-in decorator named @classmethod before the declaration of the method.
Let’s see an example:






