Alternative Syntax for Attribute Access

I’m not sure if this has come up before, but I couldn’t find it when I searched.

I’m throwing out the idea that f-strings could be use on attribute lookup for dynamic names.

So instead of this:
getattr(some_instance, name)

You could do:
some_instance.{name}

Or any other valid f-string expression (not the format specifiers)
some_instance._group{num + 1}
some_instance.{get_next_name()}

It could potentially be extended to cover the default, something like
some_instance.{name:default_value}

I’ll admit this is sugar and any performance gain from eliminating the call to getattr() is marginal. Readability is subjective, but some_instance.{name1}.{name2}
seems more readable to me than getattr(getattr(some_instance, ‘name1’), ‘name2’).

Of course this would require updating the parser and lookup logic. And we’d need to determine if nested dynamic lookups would be supported:
some_instance.{ name.{sub_name} }

Just an idea, because even if this one ends up in the bin, maybe it inspires the next great idea. Try not to be too harsh :slight_smile:

Edit 1:
Changed title from “F-Strings as attributes“ to “Alternative Syntax for Attribute Access“ to better reflect the spirit of the proposal rather than the initial concept. Author’s note added below.

Author’s Note:
It seems my initial idea wasn’t communicated as clearly as I thought, and that has led to some alternatives that may be improvements. In my initial idea, anything after or between dots was to be parsed as an f-string, but I only gave one concrete example of this, some_instance._group{num + 1}. Some interpreted the syntax as object.{…}, where is a stand in for code to be evaluated. The result of the evaluated code would need to be a string. This is likely clearer and easier to implement. @blhsing then suggested removing the dot, object{…}, so the syntax is similar to the way square brackets are used for indexing. Perhaps this is even clearer. Here are some examples of each:

Option A: Everything after or between dots can be interpreted as an f-string
some_instance.{name}, some_instance.{‘some_string’}, some_instance.group{num + 1}, some_instance.{name1}.{name2}

Option B: If the contents after or between the dots is enclosed in curly braces, it’s evaluated
some_instance.{name}, some_instance.{‘some_string’}, some_instance.{f’group{num + 1}'}, some_instance.{name1}.{name2}

Option C: No dot is used and curly braces are used for attribute access similarly to how square brackets are used for indexing
some_instance{name}, some_instance{‘some_string’}, some_instance{f’group{num + 1}'}, some_instance{name1}{name2}

12 Likes

+1
The readability improvement especially over nested getattr calls is very compelling for me.

Support for nesting should come naturally because any expression should be allowed inside curly brackets, including another obj.{name} expression.

The same notation may also be used in place of setattr and delattr calls.

The title of this thread is a bit confusing since it doesn’t involve actual f-strings. I suggest that you clarify it with something like “Accessing attributes by name with f-string-like syntax”.

By the way the dot appears to be technically redundant as a delimiter if curly brackets are enclosing the attribute name. I wonder how people feel about this alternative syntax, which draws parallel with subscript access instead (I know that for some dot may still be useful as an indicator of an attribute access):

some_instance{name1}{name2}
2 Likes

I agree that this would be nice syntactic sugar, useful and readable in some cases - but I’m concerned about how readable it is for people who don’t write in Python often?

With this new syntax, one could read:

some_instance.{some_func()}

And be confused when it doesn’t run

some_instance.some_func()

Python also already uses {some_obj} as a set literal - {13.4} is certainly not the same as " 13.4" which it would be interpreted as in this context (though, of course, one cannot currently use a set for attribute access, so this is possibly moot).

Similarly, the syntax for defaults looks suspiciously like a dictionary: inst.{"hello":2} looks like one is applying some dot operator between inst and a new dict {"hello": 2}, especially if it is formatted poorly.

But maybe these aren’t compelling enough cases given the increased readability in the cases you mention?
I’d love to see if there are examples of similar syntax in other languages and how readible it is there, or if there is a standard syntax or way of handling this kind of pattern - I don’t think I’ve ever seen it before.

2 Likes

I fail to see how just because some_instance.{some_func()} works anyone would expect that it would work without curly brackets too.

I don’t think that’s an issue because you don’t hear people complaining about confusing instance[value] with [value].

2 Likes

Thanks for the idea, but I’m -1 on it. In any code I’ve seen or written, there aren’t enough getattr calls to justify new syntax for them. Plus, this doesn’t add any expressiveness to Python: it just saves typing a few characters. I’m not convinced by the nested getattr calls: I’ve literally never seen code that does that. And the code with the nested calls doesn’t need explaining: it’s just how expressions work in Python.

27 Likes

I actually intended that anything after the dot, or between dots, would be parsed like an f-string. That’s why I included some_instance._group{num + 1}. However, I think your example, using curly braces for attribute access similar to the way square brackets are used for indexes, is likely clearer and easier to implement.

I’ve thought about the same idea before, but I’ve never seen the usefulness in this. The highest level of nested get attrs I’ve encountered/ written myself is 2-3, and if users wanted to use such syntax, one could simply evaluate it (using eval(...) with some f-string).

Therefore I’m kinda ±1 on this.

I run into it enough that it bothers me, but I think it depends on what you work on and your coding style.
I found 10 examples of nested getattr() or setattr() calls in the 3.13 stdlib. There were about the same amount in the tests.

inspect.py: Line 826 - 827 AND pydoc.py: Line 121 - 122
Note: This seems like a bug. getattr(None, '__func__') would raise an AttributeError
Original: if (isclass(self) and getattr(getattr(self, name, None), '__func__') is obj.__func__):
Option B: if (isclass(self) and self.{name:None}.__func__ is obj.__func__:

inspect.py: Line 985
Original: elif getattr(getattr(module, "__spec__", None), “loader”, None) is not None:
Option B: elif module.{'__spec__':None}.{‘loader’:None} is not None:

email/_policybase.py: Line 105
Original: doc = getattr(getattr(c, name), '__doc__')
Option B: doc = c.{name}.__doc__

importlib/_bootstrap.py line 44 AND importlib/_bootstrap_external.py line 690
Original: setattr(new, replace, getattr(old, replace))
Option B: new.{replace} = old.{replace}

importlib/_bootstrap.py line 1355 - 1356
Original: if (module is _NEEDS_LOADING or getattr(getattr(module, "__spec__“, None), “_initializing”, False)):
Option B: if module is _NEEDS_LOADING or module.{'__spec__':None}.{‘_initializing’:False}:

importlib/metadata/init.py: Line 338 - 342
Original: abstract = {name for name in all_name if getattr(getattr(cls, name), '__isabstractmethod__', False)}
Option B: abstract = {name for name in all_names if cls.{name}.{'__isabstractmethod__':False}}

Lib/tkinter/scrolledtext.py: Line 39
Original: setattr(self, m, getattr(self.frame, m))
Option B: self.{m} = self.frame.{m}

Lib/unittest/mock.py: Line 147
Original: setattr(funcopy, attribute, getattr(func, attribute))
Option B: funcopy.{attribute} = func.{attribute}

A couple things occurred to me as I went through these examples. The first is the proposed syntax is not only easier to read, but it requires fewer mental steps to decipher. The second is I think Option B, where the dots are included, ends up being easier to read than Option C, where the dots are removed, because, again, it’s fewer mental steps. Reading self.{m} = self.frame.{m}seems more consistent than reading self{m} = self.frame{m} since the dot is always there between the identifiers regardless of if they are dynamic or not.

In general, I don’t think we should add a feature that makes it easier to add attributes dynamically, because that’s typically bad practice (especially in the age of static typing). You should use a dictionary if you’re dynamically loading a bunch of arbitrary strings.

Anyways, if readability is your concern, you can get pretty close to your proposed feature by simply modifying the attribute dictionary:

# Both equivalent to self.{key} = self.something.{key}
vars(self)[key] = vars(self.something)[key]
# Or, if you don't like vars()
self.__dict__[key] = self.something.__dict__[key]

(This won’t work on objects with __slots__, but I think dynamic attribute modification is generally very uncommon for objects without a __dict__. If it really bothers you, you could invent your own dict proxy that works with __slots__.)

9 Likes

10 examples in ~300k lines of code is not a compelling reason to add new syntax. The examples you found are in famously tricky code involving metaprogramming.

I’ll echo @ZeroIntensity’s point that when people ask how to do “variable variables” it means they need a dictionary. This seems to be a very niche use case.

I’m curious what type of work you are doing in Python that this need comes up for you.

18 Likes

I don’t think that’s what this is. In fact there was nothing about adding attributes dynamically in the original post. This is about dynamic attribute access. Can you use it to do bad programming? Sure, but, if we took out everything that you can use for bad programming, we wouldn’t have a language anymore.

It’s closer to 250k lines, but I see your point. However, I was simply responding to @ericvsmith to show there are legitimate use cases and this would improve readability in those situations. But that’s just the examples of nested cases I could find with a simple AST walk. This is not limited to nested cases and improves readability for singular cases as well. Again, this is about dynamic attribute access, which is much more common. There are 760 calls for gettattr(), setattr(), and delattr() in the stdlib.

What it really comes down to is this is sugar and it’s subjective if the sugar is additive or not. You yourself have pointed out that dot access itself is sugar and technically optional. It’s very powerful sugar and fundamental to the way we use Python. But it’s also likely used in ways Guido hadn’t envisioned when he first implemented it. I wonder if you might think from the other direction and see if you can come up other ways we could use this syntax if it existed?

Since you asked, I do a lot of abstraction programming. Essentially taking something complicated or with a lot of moving parts and turning it into an externally simple API, tool, or framework that others use to extend functionality, build applications, or run tests. Naturally, this sometimes involves a more dynamic style of programming. It also involves reading a lot of code, so anything that improves readability is a win for me.

I brought up dynamically setting attributes because that was common in your examples. I think the same goes for attribute reading, though – there aren’t that many reasons to have variable attribute names. (And if you do need them, just use the attribute dictionary if you’re opposed to using getattr().)

Basically, I’m trying to say that what you’re proposing is pretty easy to replicate right now. Is there a clear win that comes with foo = something.{bar} over foo = vars(something)[bar], other than “readability”?

2 Likes

Those examples are from the standard library. They were included to show examples of nested calls to getattr() or setattr() in the wild. The filenames and line numbers are provided. If you think they could be done another way, I am sure PRs would be welcome.

What you’re suggesting isn’t equivalent. Calling vars() is slower, about 200% in a simple use case, and will raise a TypeError if slots are used. I also don’t understand why you would say “there aren’t that many reasons to have variable attribute names”. Maybe if you write very flat, static code. But it comes up often: wrapping, mocking, inspection, plugins, iteration over multiple attributes, proxied lookups, …

I also think you’re discounting readability. Code is read way more than it is written. For me, this is enough. And it’s possibly slightly more efficient because it drops the function call. But I also think we could use this as a stepping stone. You know, like “yes, and“ in improve :slight_smile:

For example, one thing I’ve been thinking about is if the syntax had null or boolean coalescing. Something like some_instance.{name??alternate} instead of alternate if (value := getattr(some_instance, name)) is None else value.

1 Like

It’s also true that library code is used much more often that it’s read.
You’re evaluating this from your own personal viewpoint as the library
author and not considering the impact on the wider Python community.
Adding new syntax to the language is a VERY big deal, for reasons others
have pointed out many times before. It’s only justified when the
benefits to the community – not just the person proposing the change –
are commensurately large.

3 Likes

I’m generally +1 on this, but like others have mentioned, I’m not sure how big the use case is.
If this were to become a feature, it would feel more natural and hopefully incentivize type checker maintainers to add appropriate hinting.

For example:

class Class:
    a: int
    b: float
    c: complex

def foo() -> Literal['a', 'b', 'c']:
   ...

inst = Class()
inst.{foo()}  # Revealed as int | float | complex

Here’s an example wrapper class that allows sugary attribute access with default values and no nesting.


class Vars:
    def __init__(self, obj: object) -> None:
        self.value = obj

    def __getitem__(self, attr_list: str|slice|tuple[str|slice, ...]) -> Any:
        if isinstance(attr_list, str):
            return getattr(self.value, attr_list)
        if isinstance(attr_list, slice):
            assert attr_list.step is None
            return getattr(self.value, attr_list.start, attr_list.stop)

        if isinstance(attr_list, tuple):
            if len(attr_list) == 0:
                return self.value
            return Vars(self.__getitem__( attr_list[0]))[attr_list[1:]]


class Example:
    class Child:
        class SubChild:
            pass

assert Vars(Example)['Child'] == Example.Child
assert Vars(Example)['Oops': None, '__class__'] == type(None)
assert Vars('bad value')['Child': None, 'SubChild': 'No Subchild Found'] == 'No Subchild Found'

1 Like