Say goodbye to boilerplate in Python with attrs

Learn more about solving common Python problems in our series covering seven PyPI libraries.
134 readers like this.
Programming at a browser, orange hands

opensource.com

Python is one of the most popular programming languages in use today—and for good reasons: it's open source, it has a wide range of uses (such as web programming, business applications, games, scientific programming, and much more), and it has a vibrant and dedicated community supporting it. This community is the reason we have such a large, diverse range of software packages available in the Python Package Index (PyPI) to extend and improve Python and solve the inevitable glitches that crop up.

In this series, we'll look at seven PyPI libraries that can help you solve common Python problems. Today, we'll examine attrs, a Python package that helps you write concise, correct code quickly.

attrs

If you have been using Python for any length of time, you are probably used to writing code like:

class Book(object):

    def __init__(self, isbn, name, author):
        self.isbn = isbn
        self.name = name
        self.author = author

Then you write a __repr__ function; otherwise, it would be hard to log instances of Book:

    def __repr__(self):
        return f"Book({self.isbn}, {self.name}, {self.author})"

Next, you write a nice docstring documenting the expected types. But you notice you forgot to add the edition and published_year attributes, so you have to modify them in five places.

What if you didn't have to?

@attr.s(auto_attribs=True)
class Book(object):
    isbn: str
    name: str
    author: str
    published_year: int
    edition: int

Annotating the attributes with types using the new type annotation syntax, attrs detects the annotations and creates a class.

ISBNs have a specific format. What if we want to enforce that format?

@attr.s(auto_attribs=True)
class Book(object):
    isbn: str = attr.ib()
    @isbn.validator
    def pattern_match(self, attribute, value):
        m = re.match(r"^(\d{3}-)\d{1,3}-\d{2,3}-\d{1,7}-\d$", value)
        if not m:
            raise ValueError("incorrect format for isbn", value)
    name: str 
    author: str
    published_year: int
    edition: int

The attrs library also has great support for immutability-style programming. Changing the first line to @attr.s(auto_attribs=True, frozen=True) means that Book is now immutable: trying to modify an attribute will raise an exception. Instead, we can get a new instance with modification using attr.evolve(old_book, published_year=old_book.published_year+1), for example, if we need to push publication forward by a year.

In the next article in this series, we'll look at singledispatch, a library that allows you to add methods to Python libraries retroactively.

Review the previous articles in this series

What to read next
Tags
Moshe sitting down, head slightly to the side. His t-shirt has Guardians of the Galaxy silhoutes against a background of sound visualization bars.
Moshe has been involved in the Linux community since 1998, helping in Linux "installation parties". He has been programming Python since 1999, and has contributed to the core Python interpreter. Moshe has been a DevOps/SRE since before those terms existed, caring deeply about software reliability, build reproducibility and other such things.

Comments are closed.

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.