Insights

10 Python Class __init__ Best Practices

Python's __init__ method is one of the most important and misunderstood concepts in object-oriented programming. Here are 10 best practices to help you get it right.

Python is an object-oriented language, and as such it provides features that support object-oriented programming (OOP). One of these features is the Python __init__ method.

The __init__ method is a special method in Python that is called when an instance of a class is created. It is used to initialize the instance of the class.

In this article, we will take a look at 10 best practices for working with the __init__ method in Python.

1. Use __init__ for initialization

The __init__ method is called when an object is created from a class, and it allows the class to initialize the attributes of the object. If you don’t initialize the attributes in the __init__ method, they will be created when the first attribute is assigned to the object.

This can lead to problems if the attributes are mutable, because changing one attribute could unintentionally change another attribute. For example, consider this code:

class Foo:
def __init__(self):
self.a = []
self.b = []

foo = Foo()
foo.a.append(1)
print(foo.b)

This code will print [1], because when you append 1 to foo.a, that list is also assigned to foo.b. This can be avoided by initializing the attributes in the __init__ method:

class Foo:
def __init__(self):
self.a = []
self.b = []

foo = Foo()
foo.a.append(1)
print(foo.b)

Now, this code will print [], because the lists are initialized in the __init__ method and are not the same list.

Initializing the attributes in the __init__ method is a good practice because it makes the code more predictable and easier to understand.

2. Don’t use __init__ for instantiation

When you use the __init__ method for instantiation, it can be difficult to change the way your objects are created later on. This is because the __init__ method is called when the class is first instantiated, and if you want to change how your objects are created, you would need to change the __init__ method itself.

Instead, it is best to create a separate method that is responsible for instantiating your objects. This way, if you ever need to change how your objects are created, you can simply change the instantiation method, without having to touch the __init__ method.

3. Avoid using mutable types as default arguments

When you use a mutable type as a default argument, that same object is used for every instance of the class. This can lead to some unexpected behavior, especially if you’re not aware that the object is being shared.

For example, let’s say you have a class with a list as a default argument:

class MyClass:
def __init__(self, my_list=[]):
self.my_list = my_list

If you create two instances of this class and add an element to the list in each instance, you might expect the list to be different in each instance. However, because the same list is being used for both instances, they will actually end up with the same list:

>>> instance1 = MyClass()
>>> instance2 = MyClass()
>>> instance1.my_list.append(‘foo’)
>>> instance2.my_list
[‘foo’]
This can obviously lead to some confusion, so it’s best to avoid using mutable types as default arguments. If you do need to use a mutable type, you can work around this issue by creating a new object in the __init__ method:

class MyClass:
def __init__(self, my_list=None):
if my_list is None:
my_list = []
self.my_list = my_list

Now, each instance will have its own list:

>>> instance1 = MyClass()
>>> instance2 = MyClass()
>>> instance1.my_list.append(‘foo’)
>>> instance2.my_list
[]

4. Avoid using class attributes

When you use class attributes, your code becomes tightly coupled to the attribute names. This makes it difficult to change the attribute names later on, without having to go through and update all your code.

It’s much better to avoid using class attributes altogether, and instead use instance attributes. This way, your code will be more flexible and easier to change in the future.

5. Prefer keyword-only arguments over positional ones

When you use positional arguments in your __init__ method, it can be easy to mix up the order of the arguments and end up with unexpected results. For example, if you have a class that takes two positional arguments, one for the name of the person and one for their age, you might accidentally swap the order when you create an instance of the class:

class Person:
def __init__(self, name, age):
self.name = name
self.age = age

# The order of the positional arguments is swapped
p1 = Person(“John”, 30)
print(p1.name) # prints “30”

On the other hand, if you use keyword-only arguments, you will always get the expected result because the keyword tells Python which value goes with which parameter:

class Person:
def __init__(self, *, name, age):
self.name = name
self.age = age

# The order of the keyword arguments doesn’t matter
p1 = Person(age=30, name=”John”)
print(p1.name) # prints “John”

As you can see, using keyword-only arguments can help prevent errors in your code.

6. Prefer self over cls in @classmethod

The Python class __init__ method is called when an instance of the class is created. The self argument refers to the instance of the class being created, while the cls argument refers to the class itself.

Using self in @classmethod makes it clear that the method is working with an instance of the class, while using cls can be confusing because it’s not immediately clear whether the method is working with an instance or the class itself.

Additionally, using self in @classmethod can make it easier to refactor code because you don’t need to worry about changing all occurrences of cls to self.

7. Prefer explicit imports over implicit ones

If you import modules implicitly in your __init__, it’s not clear where those objects come from and what their names are. This can lead to confusion and errors down the road.

It’s much better to be explicit about your imports. That way, it’s clear where everything is coming from and there’s no room for error.

8. Prefer properties over getters and setters

Getters and setters are methods that are used to get or set the value of an attribute, respectively. They are typically used when there is some sort of logic that needs to be executed when getting or setting an attribute.

For example, let’s say we have a class that represents a person. We might want to ensure that the person’s age is always greater than 0. We could do this by writing a getter and setter for the age attribute.

class Person:
def __init__(self, age):
self._age = age

@property
def age(self):
return self._age

@age.setter
def age(self, value):
if value > 0:
self._age = value
else:
raise ValueError(‘Age must be greater than 0’)
Now, whenever we try to get or set the age attribute, the corresponding getter or setter method will be called. This allows us to execute our logic to ensure that the age is always greater than 0.

While getters and setters can be useful in some situations, they also have some drawbacks. For one, they add extra boilerplate code to our classes. In the example above, we had to write 4 additional lines of code just to add basic validation to our age attribute.

Furthermore, getters and setters can make our code more difficult to understand. In the example above, it’s not immediately clear what the age property does just by looking at it. We have to look at the corresponding getter and setter methods to see that it’s doing some basic validation.

Fortunately, Python provides an alternative to using getters and setters known as properties. Properties allow us to define our attributes in a more concise way while still allowing us to execute custom logic when getting or setting those attributes.

Let’s take a look at how we would rewrite the example above using properties.

class Person:
def __init__(self, age):
self._age = age

@property
def age(self):
if self

9. Prefer composition over inheritance

When you’re using inheritance, your subclasses are tightly coupled to the parent class. This can lead to problems down the road if the parent class needs to be changed in a way that breaks the child classes.

With composition, on the other hand, your classes are not tightly coupled. This means that you can change the internals of a class without affecting the other classes that use it.

Composition also gives you more flexibility. For example, let’s say you have a class that represents a car. With inheritance, you would have to create a separate subclass for each type of car (sedan, SUV, etc.). With composition, you could simply create a class that represents a car and then create separate classes for each type of car.

So, when should you use inheritance? Inheritance is best used when you need to extend the functionality of a class without changing the existing code. For example, if you have a class that represents a shape, you might create subclasses for specific shapes like circles and squares.

10. Prefer exceptions over return codes

When an error occurs in the __init__ method, it’s often unclear what the caller should do. Should they try to recover and continue? Should they propagate the exception? Should they just log it and move on?

If you return a value from __init__ to indicate an error, the caller has to know how to handle that. They might just ignore it and continue, which could lead to unexpected behavior later on. Or they might propagate it, which might not be what you intended.

It’s much easier for the caller if you just raise an exception when an error occurs. That way, they can decide how to handle it, and they won’t have to worry about whether they’re doing the right thing.

Previous

10 Pytest Best Practices

Back to Insights
Next

10 JWT Secret Key Best Practices