Proposal
I propose adding a new keyword expr that returns the actual string representation of an expression and optionally also evaluates it. This would work similar to how the f-string expression debugging syntax (e.g. f"{x[0]=}) works.
Specification
- The
exprkeyword expects one expression after it, of which it is supposed to return the exact string as written out in the code, excluding surrounding whitespace. - No part of the expression is actually executed, but actual identifiers must be referenced.
- If an equals sign (
=) is placed after the entire expression, similar to f-stringâs debugging syntax, a tuple of two elements is returned, where the first element is the expression and the second element is the same expression evaluated. - Inside the
exprbody, it is possible to place an exclamation mark (!) instead of a dot (.) between each part of a fully qualified name. This has the effect that only the part after the exclamation mark of that fully qualified name is included.
Example code:
>>> stars = [1, 2, 3, 4, 5]
>>> expr stars[0] # Get string representation
'stars[0]'
>>>
>>> expr stars[0]
('stars[0]', 1)
>>>
>>> import math
>>>
>>> expr math.pi.as_integer_ratio
'math.pi.as_integer_ratio'
>>>
>>> expr math!pi.as_integer_ratio
'pi.as_integer_ratio'
>>>
>>> expr math.pi!as_integer_ratio
'as_integer_ratio'
>>>
>>>
Current Alternatives
Technically it is already possible to get the string representation of an expression with some f-string (ab)use, like this:
>>> expr, _, v = f"{stars[0]=}".partition("=")
>>> expr
'stars[0]'
But this has two major drawbacks:
-
The expression is always executed, no matter if desired or not. In many scenarios this is unacceptable, as the expression could run expensive code.
Using the generated value is also unpleasant or impossible, as it is converted to its__str__format already. Depending on the context, with some workarounds the value could be extracted, but that is far from an ideal solution for the given problem. -
The expression itself is cumbersome to write out every time it is needed. It also cannot be fully refactored into a function, as the actual expression cannot be passed. The best we can do is the following:
>>> def get_expr(debug_f_string: str) -> str:
>>> expr, success, _ = debug_f_string.partition("=")
>>>
>>> if not success:
>>> raise ValueError("No '=' found")
>>>
>>> return expr.strip()
>>>
>>> get_expr(f"{stars[0]=}")
'stars[0]'
Why not just write out the expression as a string manually?
In this relatively popular StackOverflow question (âGetting the name of a variable as a stringâ, with 474 upvotes as of writing this), the top solution proposes a similar solution to mine, stated above.
The top comment to that answer asks âHow is this useful in anyway ? One can only get the result âfooâ if one manually write âfooâ. It doenât solve OP problem.â and has almost as many upvotes as the actual answer.
So it seems this is a common consensus.
There are multiple advantages over just manually typing out a string:
- If at any point the expression is changed, there is no direct error, possibly leading to bigger issues in the future. Because the new
exprbody must reference valid variables, a SyntaxError is raised if that case is not given. - If an IDE is used, renaming is usually done with refactoring tools from that IDE. This approach automatically renames the string representation of the given variables for us. If the variable does not exist anymore, because it got renamed or deleted, an error will be shown.
Use cases
With Python being a highly dynamic language, often as soon as some dynamic features come into play, it becomes necessary to represent some expression as a string itself.
Example 1: __all__
It always bothered me that __all__ references strings of identifiers, because it just looked so error prone. With the new expr keyword, we could reduce its âerror pronenessâ.
class _Base: ...
class A(_Base): ...
class B(_Base): ...
__all__ = [
expr A,
expr B
]
Example 2: Metaclasses
class PersonMeta(type):
category: str
def __prepare__(cls, name: str, bases: tuple[type, ...], category_name: str) -> Mapping[str, Any]:
return {expr cls!category: category_name}
# same as {"category": category_name}
class Baker(PersonMeta, "food"):
...
Example 3: Free to use debug strings
exp, val = expr 2**32=
print(f"The calculation '{exp}' resulted in the value {val}")
What do you think?