At some point in your Python coding adventure, you may need to create custom list-like classes with modified behavior, new functionalities, or both. To do this in Python, you can inherit from an abstract base class, subclass the built-in list
class directly, or inherit from UserList
, which lives in the collections
module.
In this tutorial, you’ll learn how to:
- Create custom list-like classes by inheriting from the built-in
list
class - Build custom list-like classes by subclassing
UserList
from thecollections
module
You’ll also write some examples that’ll help you decide which parent class, list
or UserList
, to use when creating your custom list classes.
To get the most out of this tutorial, you should be familiar with Python’s built-in list
class and its standard features. You’ll also need to know the basics of object-oriented programming and understand how inheritance works in Python.
Free Download: Click here to download the source code that you’ll use to create custom list-like classes.
Creating List-Like Classes in Python
The built-in list
class is a fundamental data type in Python. Lists are useful in many situations and have tons of practical use cases. In some of these use cases, the standard functionality of Python list
may be insufficient, and you may need to create custom list-like classes to address the problem at hand.
You’ll typically find at least two reasons for creating custom list-like classes:
- Extending the regular list by adding new functionality
- Modifying the standard list’s functionality
You can also face situations in which you need to both extend and modify the list’s standard functionality.
Depending on your specific needs and skill level, you can use a few strategies to create your own custom list-like classes. You can:
- Inherit from an appropriate abstract base class, such as
MutableSequence
- Inherit from the Python built-in
list
class directly - Subclass
UserList
fromcollections
Note: In object-oriented programming, it’s common practice to use the verbs inherit and subclass interchangeably.
There are a few considerations when you’re selecting the appropriate strategy to use. Keep reading for more details.
Building a List-Like Class From an Abstract Base Class
You can create your own list-like classes by inheriting from an appropriate abstract base class (ABC), like MutableSequence
. This ABC provides generic implementations of most list
methods except for .__getitem__()
, .__setitem__()
, .__delitem__
, .__len__()
, and .insert()
. So, when inheriting from this class, you’ll have to implement these methods yourself.
Writing your own implementation for all these special methods is a fair amount of work. It’s error-prone and requires advanced knowledge of Python and its data model. It can also imply performance issues because you’ll be writing the methods in pure Python.
Additionally, suppose you need to customize the functionality of any other standard list method, like .append()
or .insert()
. In that case, you’ll have to override the default implementation and provide a suitable implementation that fulfills your needs.
The main advantage of this strategy for creating list-like classes is that the parent ABC class will alert you if you miss any required methods in your custom implementation.
In general, you should embrace this strategy only if you need a list-like class that’s fundamentally different from the built-in list
class.
In this tutorial, you’ll focus on creating list-like classes by inheriting from the built-in list
class and the UserList
class from the standard-library collections
module. These strategies seem to be the quickest and most practical ones.
Inheriting From Python’s Built-in list
Class
For a long time, it was impossible to inherit directly from Python types implemented in C. Python 2.2 fixed this issue. Now you can subclass built-in types, including list
. This change has brought several technical advantages to the subclasses because now they:
- Will work in every place that requires the original built-in type
- Can define new instance, static, and class methods
- Can store their instance attributes in a
.__slots__
class attribute, which essentially replaces the.__dict__
attribute
The first item in this list may be a requirement for C code that expects a Python built-in class. The second item allows you to add new functionality on top of the standard list behavior. Finally, the third item will enable you to restrict the attributes of a subclass to only those attributes predefined in .__slots__
.
To kick things off and start creating custom list-like classes, say that you need a list that automatically stores all its items as strings. Assuming that your custom list will store numbers as strings only, you can create the following subclass of list
:
# string_list.py
class StringList(list):
def __init__(self, iterable):
super().__init__(str(item) for item in iterable)
def __setitem__(self, index, item):
super().__setitem__(index, str(item))
def insert(self, index, item):
super().insert(index, str(item))
def append(self, item):
super().append(str(item))
def extend(self, other):
if isinstance(other, type(self)):
super().extend(other)
else:
super().extend(str(item) for item in other)
Your StringList
class subclasses list
directly, which means that it’ll inherit all the functionality of a standard Python list
. Because you want your list to store items as strings, you need to modify all the methods that add or modify items in the underlying list. Those methods include the following:
.__init__
initializes all the class’s new instances..__setitem__()
allows you to assign a new value to an existing item using the item’s index, like ina_list[index] = item
..insert()
allows you to insert a new item at a given position in the underlying list using the item’s index..append()
adds a single new item at the end of the underlying list..extend()
adds a series of items to the end of the list.
The other methods that your StringList
class inherited from list
work just fine because they don’t add or update items in your custom list.
Note: If you want your StringList
class to support concatenation with the plus operator (+
), then you’ll also need to implement other special methods, such as .__add__()
, .__radd__()
, and .__iadd__()
.
To use StringList
in your code, you can do something like this:
>>> from string_list import StringList
>>> data = StringList([1, 2, 2, 4, 5])
>>> data
['1', '2', '2', '4', '5']
>>> data.append(6)
>>> data
['1', '2', '2', '4', '5', '6']
>>> data.insert(0, 0)
>>> data
['0', '1', '2', '2', '4', '5', '6']
>>> data.extend([7, 8, 9])
>>> data
['0', '1', '2', '2', '4', '5', '6', '7', '8', '9']
>>> data[3] = 3
>>> data
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
Your class works as expected. It converts all the input values into strings on the fly. That’s cool, isn’t it? When you create a new instance of StringList
, the class’s initializer takes care of the conversion.
When you append, insert, extend, or assign new values to the class’s instances, the methods that support each operation will take care of the string conversion process. This way, your list will always store its items as string objects.
Subclassing UserList
From collections
Another way to create a custom list-like class is to use the UserList
class from the collections
module. This class is a wrapper around the built-in list
type. It was designed for creating list-like objects back when it wasn’t possible to inherit from the built-in list
class directly.
Even though the need for this class has been partially supplanted by the possibility of directly subclassing the built-in list
class, UserList
is still available in the standard library, both for convenience and for backward compatibility.
The distinguishing feature of UserList
is that it gives you access to its .data
attribute, which can facilitate the creation of your custom lists because you don’t need to use super()
all the time. The .data
attribute holds a regular Python list
, which is empty by default.
Here’s how you can reimplement your StringList
class by inheriting from UserList
:
# string_list.py
from collections import UserList
class StringList(UserList):
def __init__(self, iterable):
super().__init__(str(item) for item in iterable)
def __setitem__(self, index, item):
self.data[index] = str(item)
def insert(self, index, item):
self.data.insert(index, str(item))
def append(self, item):
self.data.append(str(item))
def extend(self, other):
if isinstance(other, type(self)):
self.data.extend(other)
else:
self.data.extend(str(item) for item in other)
In this example, having access to the .data
attribute allows you to code the class in a more straightforward way by using delegation, which means that the list in .data
takes care of handling all the requests.
Now you almost don’t have to use advanced tools like super()
. You just need to call this function in the class initializer to prevent problems in further inheritance scenarios. In the rest of the methods, you just take advantage of .data
, which holds a regular Python list. Working with lists is a skill that you probably already have.
Note: In the above example, you’ll be okay if you reuse the original internal implementation of StringList
from the previous section but change the parent class from list
to UserList
. Your code will work the same. However, using .data
can facilitate the process of coding list-like classes.
This new version works the same as your first version of StringList
. Go ahead and run the following code to try it out:
>>> from string_list import StringList
>>> data = StringList([1, 2, 2, 4, 5])
>>> data
['1', '2', '2', '4', '5']
>>> data.append(6)
>>> data
['1', '2', '2', '4', '5', '6']
>>> data.insert(0, 0)
>>> data
['0', '1', '2', '2', '4', '5', '6']
>>> data.extend([7, 8, 9])
>>> data
['0', '1', '2', '2', '4', '5', '6', '7', '8', '9']
>>> data[3] = 3
>>> data
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
Exposing .data
is the most relevant feature of UserList
, as you’ve already learned. This attribute can simplify your classes because you don’t need to use super()
all the time. You can just take advantage of .data
and use the familiar list
interface to work with this attribute.
Coding List-Like Classes: Practical Examples
You already know how to use list
and UserList
when you need to create custom list-like classes that add or modify the standard functionality of list
.
Admittedly, when you think of creating a list-like class, inheriting from list
probably seems more natural than inheriting from UserList
because Python developers know about list
. They might not be aware of the existence of UserList
.
You also know that the main difference between these two classes is that when you inherit from UserList
, you have access to the .data
attribute, which is a regular list that you can manipulate through the standard list
interface. In contrast, inheriting from list
requires advanced knowledge about Python’s data model, including tools like the built-in super()
function and some special methods.
In the following sections, you’ll code a few practical examples using both classes. After writing these examples, you’ll be better prepared to select the right tool to use when you need to define custom list-like classes in your code.
A List That Accepts Numeric Data Only
As a first example of creating a list-like class with custom behavior, say that you need a list that accepts numeric data only. Your list should store only integer, float, and complex numbers. If you try to store a value of any other data type, like a string, then your list should raise a TypeError
.
Here’s an implementation of a NumberList
class with the desired functionality:
# number_list.py
class NumberList(list):
def __init__(self, iterable):
super().__init__(self._validate_number(item) for item in iterable)
def __setitem__(self, index, item):
super().__setitem__(index, self._validate_number(item))
def insert(self, index, item):
super().insert(index, self._validate_number(item))
def append(self, item):
super().append(self._validate_number(item))
def extend(self, other):
if isinstance(other, type(self)):
super().extend(other)
else:
super().extend(self._validate_number(item) for item in other)
def _validate_number(self, value):
if isinstance(value, (int, float, complex)):
return value
raise TypeError(
f"numeric value expected, got {type(value).__name__}"
)
In this example, your NumberList
class inherits directly from list
. This means that your class shares all the core functionality with the built-in list
class. You can iterate over instances of NumberList
, access and update its items using their indices, call common list
methods, and more.
Now, to ensure that every input item is a number, you need to validate each item in all the methods that support operations for adding new items or updating existing items in the list. The required methods are the same as in the StringList
example back in the Inheriting From Python’s Built-In list
class section.
To validate the input data, you use a helper method called ._validate_number()
. This method uses the built-in isinstance()
function to check if the current input value is an instance of int
, float
, or complex
, which are the built-in classes representing numeric values in Python.
Note: A more generic way to check whether a value is a number in Python would be to use Number
from the numbers
module. This will allow you to validate Fraction
and Decimal
objects too.
If the input value is an instance of a numeric data type, then your helper function returns the value itself. Otherwise, the function raises a TypeError
exception with an appropriate error message.
To use NumberList
, go back to your interactive session and run the following code:
>>> from number_list import NumberList
>>> numbers = NumberList([1.1, 2, 3j])
>>> numbers
[1.1, 2, 3j]
>>> numbers.append("4.2")
Traceback (most recent call last):
...
TypeError: numeric value expected, got str
>>> numbers.append(4.2)
>>> numbers
[1.1, 2, 3j, 4.2]
>>> numbers.insert(0, "0")
Traceback (most recent call last):
...
TypeError: numeric value expected, got str
>>> numbers.insert(0, 0)
>>> numbers
[0, 1.1, 2, 3j, 4.2]
>>> numbers.extend(["5.3", "6"])
Traceback (most recent call last):
...
TypeError: numeric value expected, got str
>>> numbers.extend([5.3, 6])
>>> numbers
[0, 1.1, 2, 3j, 4.2, 5.3, 6]
In these examples, the operations that add or modify data in numbers
automatically validate the input to ensure that only numeric values are accepted. If you add a string value to numbers
, then you get a TypeError
.
An alternative implementation of NumberList
using UserList
can look something like this:
# number_list.py
from collections import UserList
class NumberList(UserList):
def __init__(self, iterable):
super().__init__(self._validate_number(item) for item in iterable)
def __setitem__(self, index, item):
self.data[index] = self._validate_number(item)
def insert(self, index, item):
self.data.insert(index, self._validate_number(item))
def append(self, item):
self.data.append(self._validate_number(item))
def extend(self, other):
if isinstance(other, type(self)):
self.data.extend(other)
else:
self.data.extend(self._validate_number(item) for item in other)
def _validate_number(self, value):
if isinstance(value, (int, float, complex)):
return value
raise TypeError(
f"numeric value expected, got {type(value).__name__}"
)
In this new implementation of NumberList
, you inherit from UserList
. Again, your class will share all the core functionality with a regular list
.
In this example, instead of using super()
all the time to access methods and attributes in the parent class, you use the .data
attribute directly. To some extent, using .data
arguably simplifies your code compared to using super()
and other advanced tools like special methods.
Note that you only use super()
in the class initializer, .__init__()
. This is a best practice when you’re working with inheritance in Python. It allows you to properly initialize attributes in the parent class without breaking things.
A List With Additional Functionality
Now say that you need a list-like class with all the standard functionality of a regular Python list
. Your class should also provide some extra functionality borrowed from the Array data type of JavaScript. For example, you’ll need to have methods like the following:
.join()
concatenates all the list’s items in a single string..map(action)
yields new items that result from applying anaction()
callable to each item in the underlying list..filter(predicate)
yields all the items that returnTrue
when callingpredicate()
on them..for_each(func)
callsfunc()
on every item in the underlying list to generate some side effect.
Here’s a class that implements all these new features by subclassing list
:
# custom_list.py
class CustomList(list):
def join(self, separator=" "):
return separator.join(str(item) for item in self)
def map(self, action):
return type(self)(action(item) for item in self)
def filter(self, predicate):
return type(self)(item for item in self if predicate(item))
def for_each(self, func):
for item in self:
func(item)
The .join()
method in CustomList
takes a separator character as an argument and uses it to concatenate the items in the current list object, which is represented by self
. To do this, you use str.join()
with a generator expression as an argument. This generator expression converts every item into a string object using str()
.
The .map()
method returns a CustomList
object. To construct this object, you use a generator expression that applies action()
to every item in the current object, self
. Note that the action can be any callable that takes an item as an argument and returns a transformed item.
The .filter()
method also returns a CustomList
object. To build this object, you use a generator expression that yields the items for which predicate()
returns True
. In this case, predicate()
must be a Boolean-valued function that returns True
or False
depending on certain conditions applied to the input item.
Finally, the .for_each()
method calls func()
on every item in the underlying list. This call doesn’t return anything but triggers some side effects, as you’ll see below.
To use this class in your code, you can do something like the following:
>>> from custom_list import CustomList
>>> words = CustomList(
... [
... "Hello,",
... "Pythonista!",
... "Welcome",
... "to",
... "Real",
... "Python!"
... ]
... )
>>> words.join()
'Hello, Pythonista! Welcome to Real Python!'
>>> words.map(str.upper)
['HELLO,', 'PYTHONISTA!', 'WELCOME', 'TO', 'REAL', 'PYTHON!']
>>> words.filter(lambda word: word.startswith("Py"))
['Pythonista!', 'Python!']
>>> words.for_each(print)
Hello,
Pythonista!
Welcome
to
Real
Python!
In these examples, you first call .join()
on words
. This method returns a unique string that results from concatenating all the items in the underlying list.
The call to .map()
returns a CustomList
object containing uppercased words. This transformation results from applying str.upper()
to all the items in words
. This method works pretty similarly to the built-in map()
function. The main difference is that instead of returning a list, the built-in map()
function returns an iterator that yields transformed items lazily.
The .filter()
method takes a lambda
function as an argument. In the example, this lambda
function uses str.startswith()
to select those words that start with the "Py"
prefix. Note that this method works similarly to the built-in filter()
function, which returns an iterator instead of a list.
Finally, the call to .for_each()
on words
prints every word to the screen as a side effect of calling print()
on each item in the underlying list. Note that the function passed to .for_each()
should take an item as an argument, but it shouldn’t return any fruitful value.
You can also implement CustomList
by inheriting from UserList
rather than from list
. In this case, you don’t need to change the internal implementation, just the base class:
# custom_list.py
from collections import UserList
class CustomList(UserList):
def join(self, separator=" "):
return separator.join(str(item) for item in self)
def map(self, action):
return type(self)(action(item) for item in self)
def filter(self, predicate):
return type(self)(item for item in self if predicate(item))
def for_each(self, func):
for item in self:
func(item)
Note that in this example, you just changed the parent class. There’s no need to use .data
directly. However, you can use it if you want. The advantage is that you’ll provide more context to other developers reading your code:
# custom_list.py
from collections import UserList
class CustomList(UserList):
def join(self, separator=" "):
return separator.join(str(item) for item in self.data)
def map(self, action):
return type(self)(action(item) for item in self.data)
def filter(self, predicate):
return type(self)(item for item in self.data if predicate(item))
def for_each(self, func):
for item in self.data:
func(item)
In this new version of CustomList()
, the only change is that you’ve replaced self
with self.data
to make it clear that you’re working with a UserList
subclass. This change makes your code more explicit.
Considering Performance: list
vs UserList
Up to this point, you’ve learned how to create your own list-like classes by inheriting from either list
or UserList
. You also know that the only visible difference between these two classes is that UserList
exposes the .data
attribute, which can facilitate the coding process.
In this section, you’ll consider an aspect that can be important when it comes to deciding whether to use list
or UserList
to create your custom list-like classes. That’s performance!
To evaluate if there are performance differences between classes that inherit from list
vs UserList
, you’ll use the StringList
class. Go ahead and create a new Python file containing the following code:
# performance.py
from collections import UserList
class StringList_list(list):
def __init__(self, iterable):
super().__init__(str(item) for item in iterable)
def __setitem__(self, index, item):
super().__setitem__(index, str(item))
def insert(self, index, item):
super().insert(index, str(item))
def append(self, item):
super().append(str(item))
def extend(self, other):
if isinstance(other, type(self)):
super().extend(other)
else:
super().extend(str(item) for item in other)
class StringList_UserList(UserList):
def __init__(self, iterable):
super().__init__(str(item) for item in iterable)
def __setitem__(self, index, item):
self.data[index] = str(item)
def insert(self, index, item):
self.data.insert(index, str(item))
def append(self, item):
self.data.append(str(item))
def extend(self, other):
if isinstance(other, type(self)):
self.data.extend(other)
else:
self.data.extend(str(item) for item in other)
These two classes work the same. However, they’re internally different. StringList_list
inherits from list
, and its implementation is based on super()
. In contrast, StringList_UserList
inherits from UserList
, and its implementation relies on the internal .data
attribute.
To compare the performance of these two classes, you should begin by timing standard list operations, such as instantiation. However, in these examples, both initializers are equivalent, so they should perform the same.
Measuring the execution time of new functionalities is also useful. For example, you can check the execution time of .extend()
. Go ahead and run the following code:
>>> import timeit
>>> from performance import StringList_list, StringList_UserList
>>> init_data = range(10000)
>>> extended_list = StringList_list(init_data)
>>> list_extend = min(
... timeit.repeat(
... stmt="extended_list.extend(init_data)",
... number=5,
... repeat=2,
... globals=globals(),
... )
... ) * 1e6
>>> extended_user_list = StringList_UserList(init_data)
>>> user_list_extend = min(
... timeit.repeat(
... stmt="extended_user_list.extend(init_data)",
... number=5,
... repeat=2,
... globals=globals(),
... )
... ) * 1e6
>>> f"StringList_list().extend() time: {list_extend:.2f} μs"
'StringList_list().extend() time: 4632.08 μs'
>>> f"StringList_UserList().extend() time: {user_list_extend:.2f} μs"
'StringList_UserList().extend() time: 4612.62 μs'
In this performance test, you use the timeit
module along with the min()
function to measure the execution time of a piece of code. The target code consists of calls to .extend()
on instances of StringList_list
and StringList_UserList
using some sample data.
The performance difference between the class based on list
and the class based on UserList
is mostly nonexistent in this example.
Often, when you create a custom list-like class, you’d expect subclasses of list
to perform better than subclasses of UserList
. Why? Because list
is written in C and optimized for performance, while UserList
is a wrapper class written in pure Python.
However, in the above example, it looks like this assumption isn’t completely right. For this reason, to decide which superclass is best for your specific use case, make sure to run a performance test.
Performance aside, inheriting from list
is arguably the natural way in Python, mostly because list
is directly available to Python developers as a built-in class. Additionally, most Python developers will be familiar with lists and their standard features, which will allow them to write list-like classes more quickly.
In contrast, the UserList
class lives in the collections
module, meaning that you’ll have to import it if you want to use it in your code. Additionally, not all Python developers are aware of the existence of UserList
. However, UserList
can still be a useful tool because of the convenience of accessing the .data
attribute, which can facilitate the creation of custom list-like classes.
Conclusion
You’ve now learned how to create custom list-like classes with modified and new behaviors. To do this, you’ve subclassed the built-in list
class directly. As an alternative, you’ve also inherited from the UserList
class, which is available in the collections
module.
Inheriting from list
and subclassing UserList
are both suitable strategies for approaching the problem of creating your own list-like classes in Python.
In this tutorial, you learned how to:
- Create list-like classes by inheriting from the built-in
list
class - Build list-like classes by subclassing
UserList
from thecollections
module
Now you’re better prepared to create your own custom lists, allowing you to leverage the full power of this useful and commonplace data type in Python.
Free Download: Click here to download the source code that you’ll use to create custom list-like classes.