Summary

This example illustrates how one can change the indentation of an entire block of code, eliminate lines, and change the content much more drastically than what the previous examples have done.

The idea behind this example is to help reduce the amount of typing required and increases readability when assigning attributes in a class’s __init__() method.

Auto self

Python is known for its concise and readable syntax. One exception about the concisiveness is the boiler plate code that has to be written when defining one’s own class, especially if it has many attributes, like:

self.this_variable = this_variable
self.that_variable = that_variable
self.this_other_variable = this_other_variable
self.foo = foo
self.bar = bar

self.baz = [] if baz is None else baz
self.spam = bread + ham

This leads people to ask on various forums, such as this question on StackOverflow, how to do automatic assignment of attributes. The answers most often given are:

  • Don’t do it; learn to live with the explicit self.

  • Use a decorator, with various examples provided.

As programmers create more classes, they find the need to add their own dunder methods, such as __eq__(self, other), __repr__(self), etc. Eventually, they might get annoyed enough at having to re-create these methods too often, with the occasional typo causing bugs that they jump with joy when discovering attrs: Classes Without Boilerplate.

Starting with Python 3.7, the standard library includes dataclasses which shares some similarity with attrs. However, it does require to use type hints which, in my opinion, reduces readability; note that many programmers find that type hints increase readability.

As a concrete example of using traditional Python notation and dataclasses, let’s consider the code given in PEP 557 but reformatted with Black:

class Application:
    def __init__(
        self,
        name,
        requirements,
        constraints=None,
        path="",
        executable_links=None,
        executables_dir=(),
    ):
        self.name = name
        self.requirements = requirements
        self.constraints = {} if constraints is None else constraints
        self.path = path
        self.executable_links = [] if executable_links is None else executable_links
        self.executables_dir = executables_dir
        self.additional_items = []

From the same PEP document, this is the proposed code which gives the same initialization, but using the @dataclass decorator:

from dataclasses import dataclass

@dataclass
class Application:
    name: str
    requirements: List[Requirement]
    constraints: Dict[str, str] = field(default_factory=dict)
    path: str = ''
    executable_links: List[str] = field(default_factory=list)
    executable_dir: Tuple[str] = ()
    additional_items: List[str] = field(init=False, default_factory=list)

This code does more than simply initializing the variables, but I do not find it particularly readable.

So, I was wondering if it might be possible to imagine a simpler syntax. auto_self is what I came up with.

That ship has sailed …

I realize that there is zero chance that the following syntax would be adopted, especially now that the dataclasses module has been added to the Python standard library. Still, you can try it out using auto_self hook.

class Application:
    def __init__(
        self,
        name,
        requirements,
        constraints=None,
        path="",
        executable_links=None,
        executables_dir=(),
    ):
        self .= :
            name
            requirements
            constraints = {} if __ is None else __
            path
            executable_links = [] if __ is None else __
            executables_dir
            additional_items = []

Here, I am using a new operator, .=, which is meant to represent the automatic assignment of a variable to the name that precedes it (self in this example). I have seen this idea for such an operator before on python-ideas but never for introducing a code block as I do here.

By design, any dunder (double underscore), __, is taken to be equivalent to the variable being initialized. I chose a dunder instead of a single underscore _ so that it could be used in a REPL without creating conflicts with the existing use of a single underscore in Python’s REPL. I also find that it makes it more readable.

Of course, one is not restricted to using self, or having to use __ everywhere. The following is completely equivalent - although I now find it less readable, having been used to seeing __ as easy to scan placeholder:

class Application:
    def __init__(
        cls,
        name,
        requirements,
        constraints=None,
        path="",
        executable_links=None,
        executables_dir=(),
    ):
        cls .= :
            name
            requirements
            constraints = {} if constraints is None else constraints
            path
            executable_links = [] if __ is None else executable_links
            executables_dir

        cls.additional_items = []

Warning

Unlike @dataclass or attrs, no additional method is created by auto_self.

ideas.examples.auto_self.add_hook(**_kwargs)[source]

Creates and adds the import hook in sys.meta_path

ideas.examples.auto_self.transform_source(source, **_kwargs)[source]

Replaces code like:

self .= :
    a
    b
    c = this if __ == that else ___

by:

self.a = a
self.b = b
self.c = this if c == that else c