Python's Self Type | Annotate Method That Return Self Type
Sometimes we found lost in a big repository of Python code and struggled to keep track of the intended types of variables. The type hint and annotation can be helpful to cover the variable types in such scenarios. In this tutorial, we will discuss the annotated method that returns self or other instances of the class itself. We have discussed the various types of annotation and methods in the previous tutorial.
How to Annotate a Method with the Self Type in Python
Starting from Python version 3.11 and beyond, the Self type, as defined by PEP 673, is the recommended annotation for methods that return an instance of their class. The Self type can be directly imported from the `typing` module.
However, for Python versions earlier than 3.11, the Self type is available in the `typing_extensions` module. You can import it from there to use it for annotating methods that return an instance of their class.
Here's an example of how you can import and use the Self type:
In both cases, the Self type is used as an annotation for the `self` parameter and as the return type of the method. This indicates that the method returns an instance of the same class that it belongs to.
Note that for Python versions earlier than 3.11, we'll need to install the `typing_extensions` package if it's not already available. We can install it using pip:
By utilizing the Self type, we can provide clear and concise annotations for methods that return instances of their class, improving code readability and maintainability.
Now, let's see the structure of the class that represents the state and logic of a bank account. The BankAccount class includes the various options like depositing and withdrawing funds that update the state of the account and return the class instance. Let's understand the following example.
The Self type is used as an annotation for the methods that return an instance of the class.
The @dataclass decorator is applied to the BankAccount class to automatically generate default implementations of __init__, __repr__, __eq__, and other special methods based on the defined fields.
We can create a BankAccount object and use the methods as follows:
The above code creates a BankAccount object with an account number of 123456 and an initial balance of Rs. 1000.0. It then displays the account balance, deposits Rs. 500.0, withdraws Rs. 200.0, and displays the updated balance again.
Using the Self type allows for method chaining, enabling concise and readable code when performing multiple actions on a BankAccount object.
The Self type can also be used in the class methods and inheritance hierarchies. For instance - if a parent and its child class have methods that return self, we can annotate both with the Self type.
We are creating a SavingAccount class that inherits from BankAccount.
The create_from_application method is a class method (decorated with @classmethod) that creates a SavingsAccount objects based on the provided initial deposit and interest rate. It generates a random seven-digit account number using the random.randint function and then instantiates and returns a new instance of the SavingsAccount class with the generated account number, initial deposit, and interest rate.
The calculate_interest() method calculates the interest earned on the savings account based on the current balance and the interest rate. It multiplies the balance by the interest rate divided by 100 and returns the calculated interest.
The add_interest() method adds the calculated interest to the savings account. It calls the calculate_interest() method to determine the interest amount and then uses the deposit method (presumably inherited from the BankAccount class) to add the interest to the account's balance. Finally, it returns self, allowing for method chaining and enabling concise code when adding interest to an account.
We can now create a SavingsAccount object using the create_from_application() method.
Annotating with TypeVar
An alternative approach to annotate methods that return an instance of their class is by utilizing TypeVar. A TypeVar represents a type variable, which acts as a placeholder for a specific type during type checking. It is commonly used when working with generic types, such as specifying lists of specific objects like `list[str]` or `list[BankAccount]`.
By using `TypeVar`, you can define a type variable that represents the current class and use it as an annotation for methods that return an instance of that class. This allows for more flexible and generic type annotations.
Following is an example to illustrate this approach.
In this example, T is a type variable defined using TypeVar. It represents the current class, allowing for a generic annotation. The some_method() method is annotated with T as the return type, indicating that it returns an instance of the class.
By using TypeVar in this way, you can provide a more generic and flexible type annotation for methods that return instances of their class.
A TypeVar that is bound by a class can be instantiated as any subclass of that class. This feature is particularly useful in the context of the BankAccount and SavingsAccount examples.
As we can see that, TBankAccount is bound to BankAccount, now we can properly annotate the method that returns self in BankAccount.
In the above code, we annotate .display_balance() with TBankAccount to specify that it will return the class instance. The point is to remember that TBankAccount isn't the same as BankAccount. It only serves to represent the BankAccount type in annotation where we can't use BankAccount directly.
The Self type is often preferred over using TypeVar for several reasons. One of the main drawbacks of TypeVar is that it can be verbose, leading to more complex type annotations. Developers may also accidentally forget to instantiate a TypeVar or properly bind it to a class, resulting in potential type-related issues.
On the other hand, the Self type, introduced in Python 3.11 and available in typing_extensions for earlier versions, provides a simpler and more concise syntax. It explicitly represents the class instance as the return type, eliminating the need for using TypeVar.
The Self type improves code readability and reduces the risk of mistakes or oversights related to type annotations. It also enjoys better IDE support, as most modern IDEs can understand and provide accurate suggestions based on the Self type.
Considering these reasons, the Self type is generally favored over TypeVar when annotating methods that return an instance of their class due to its intuitive and concise syntax, improved code readability, and better IDE support.
Using the __future__ Module
Python's __future__module provides various approaches to annotating methods that returns the enclosing class. Sometimes, this module is used to introduce incompatible changes that are intended to become part of a future Python version.
The annotations can be imported using the greater version than 3.7 of Python. Let's understand the following example.
In the above code, we import the annotations from __future__ module. This module is not available in a lower version than 3.7. We use the class name directly as annotation for .push().
Using the future module to annotate methods with the class name is not considered a best practice, as it is less intuitive and Pythonic compared to using the Self type. Additionally, having to remember to import from the future at the beginning of the script can be cumbersome. Moreover, it is worth noting that inheritance is not adequately supported when annotating with future.
Type Hinting With String
Another option for annotating methods that return class instances is to use strings. This approach is recommended for Python versions prior to 3.7 or when none of the other methods are suitable. String annotation does not require any imports, and it is widely recognized by most static type checkers.
In this scenario, it is important to include the name of the class within the string annotation. Omitting the class name would result in the static type checker not recognizing the return type as a valid Python object. String annotations directly achieve a similar outcome as the behind-the-scenes functionality of __future__ annotations.
A significant limitation of string annotations is that they do not retain their information during inheritance. When a subclass inherits a method from a superclass, the string annotations specified in the superclass do not automatically transfer to the subclass. Consequently, if you depend on string annotations for type hinting or documentation, you will have to declare the annotations in each subclass. This process can be prone to errors and time-consuming.
Using type hints and annotations in Python can enhance the readability and maintainability of your code, particularly as it becomes larger and more intricate. By specifying the types of variables, function arguments, and return values, you assist other developers in comprehending the intended variable types and the expected outcomes of function calls.
The Self type is a specific type hint that can be utilized to annotate a method that returns an instance of its own class. By using the Self type, the return type becomes explicit, which can be advantageous in preventing subtle bugs that may occur during inheritance and subclassing. Although alternative options such as TypeVar, the __future__ module, and strings can be used to annotate methods returning class instances, it is recommended to employ the Self type whenever feasible.