Background
PEP-736 proposes a syntactic shorthand for forwarding variables to functions that accept kwargs.
Example of PEP-736's shorthand for calling with named parameters
Before:
myfunc(my_first_var=my_first_var, my_second_var=my_second_var,
my_third_var=my_third_var)
After:
myfunc(my_first_var=, my_second_var=, my_third_var=)
I am much in favor of PEP-736. I think it addresses a very common coding pattern in a way that saves keystrokes, screen real-estate, and visual bandwidth. I also think the resulting code might be easier or less error-prone to maintain.
PEP-736 also mentions an effect of the new syntax: a new shorthand for dictionary initialization, which – the PEP also points out – is favorably reminiscent of JavaScript’s shorthand properties.
Before:
return {"my_first_var": my_first_var, "my_second_var": my_second_var,
"my_third_var": my_third_var}
After:
return dict(my_first_var=, my_second_var=, my_third_var=)
Even Better Object Initialization Shorthand
While the dict initialization shorthand that would result from PEP-736 is nice, I think we could do even better. Motivation:
-
Many prefer using the
{}syntax for initializing objects over thedict()syntax:
It would be nice to be able to continue use{}while still benefiting from the spirit of PEP-736. -
I am jealous of JavaScript’s object initialization property shorthand
JS Object Initialization Shorthand Example
Before
return {my_first_var: my_first_var, my_second_var: my_second_var,
my_third_var: my_third_var}
After
return {my_first_var, my_second_var, my_third_var}
NB: Python cannot adopt the identical syntax from JavaScript because a colonless { } initializer in Python is already spoken for: it is a shorthand for initializing a set().
*I am still in favor of everything in PEP-736 – I do not here propose to change it or make any part of it unnecessary.
Request for Proposal
I think it would be great if Python had a shorthand for object initialization that is in the spirit of PEP-738 and nearly as ergonomic as JavaScript’s shorthand properties.
I would like to hear the python.org community’s feedback: do you like this idea in principle? Do you have ideas for a reasonable syntax?
Here I’ll share my own proposal (and some thoughts on a few alternatives):
f-dicts
Before:
return {"my_first_var": my_first_var, "my_second_var": my_second_var,
"my_third_var": my_third_var}
After:
return f{my_first_var, my_second_var, my_third_var}
That’s all there is to it!
f-dicts: some more detail
Shorthand can be mixed with longhand (as with PEP-736 and JS)...
For purpose of illustration, assume the following:
my_first_var = "first val"
my_second_var = "second val"
my_third_var = "third val"
Then, all of these are equivalent:
d = f{my_first_var, my_second_var, my_third_var}
d = f{my_first_var, "my_second_var": "second val", my_third_var}
d = f{"my_first_var": my_first_var, "my_second_var": my_second_var,
"my_third_var": my_third_var}
print(repr(d)) for any of the above prints:
{'my_first_var': 'first val', 'my_second_var': 'second val', 'my_third_var': 'third val'}
Syntax errors
Mixing “shorthand” with longhand in a classic {} initializer is still a syntax error, as it is today:
d = {my_first_var, my_second_var: "second val"} # invalid set literal
d = {"my_first_var": "first val", my_second_var} # invalid dict literal
Computed key confusion
An opportunity for confusion and mistakes arises when mixing shorthand notation with longhand notation in an f-dict. In the following example:
my_first_var = "first val"
my_second_var = "second val"
d = f{my_first_var, my_second_var: "custom value"}
… which result did do you expect? You could make an argument for either result being understandable:
# result 1
{'my_first_var': 'first val', "my_second_var": "custom value"}
# result 2
{'my_first_var': 'first val', "second val": "custom value"}
For the sake of simplicity, I suggest that using computed key names in an f-dict is a syntax error. If you need a computed key name, here are your workarounds:
- Don’t use an f-dict
- Add the computed key after the initializer (
d[runtime_key] = 'whatever'). - Unpack the computed key from a
{}dict into the f-dict
d = f{my_first_var, **{my_second_var: "custom value"}}
- If your desired result was result #1, simply put quotes around
my_second_var:
d = f{my_first_var, "my_second_var": "custom value"}
Comprehensions
I suspect that f-dicts are not very useful in the context of a dict comprehension. The variable names typically used within a comprehension are often temporary and/or non-descript. In order to keep the proposal simple, I suggest that if X is a generator expression, then f{X} is a syntax error. I’d like to hear opinions, though.
Unpacking
Unpacking into an f-dict is no different than unpacking into a regular dict.
if f{<expr1>} == {<expr2>}:
assert f{<expr1>, **anydict} == {<expr2>, **anydict}
assert f{<expr1>, *anyiter} == {<expr2>, *anyiter}
Pattern matching
An f-dict literal f{} creates a normal dict object in memory no later than a normal dict literal {} would. I do not see that the treatment of f{} in the context of pattern matching would be any different than {}.
Too similar to a set initializer?
A downside to the f-dict literal is that it might look too much like a set-initializer. In
myvar = f{my_first_var, my_second_var}
an uninformed reader could reasonably assume that they are reading a set-initializer, because it looks nearly identical to one:
myset = {my_first_var, my_second_var}
But then, the uninformed reader might assume that after s = f"hello {yourname}", the variable s will contain the text hello {yourname}. Or that s = f"{shutil.rmtree('/')}" would not delete their hard drive. :)
Additionally, I could not imagine (and I tried) any reasonable circumstance under which a hypothetical “f-set” would ever be needed.
Is there any connection between the `f ` in f{} and the `f ` in f-strings?
Is there any connection between the f in f{} and the f in f""?
A connection between the f in f-strings and the f in an f-dict is not obvious. With f-strings, the f is pretty clearly a mnemonic for the concept of formatting. The shorthand enabled by f-dict’s f{} syntax might not seem to have any connection to the concept of formatting at all.
There is some commonality, I think, though. Consider
f"{var=}"
Suffixing, with =, an expression embedded in an f-string via {} also converts a variable name (or even literal program expression code) into (part of) a runtime string. In a similar way, f{var} converts a symbol (var) in the program to a string "var", that is then used as a dict key. I think this is the strongest connection between f in f{} and f in f""
However, I admit that the connection is loose enough that it could be a tough buy for many people.
I considered some alternate syntax variations:
[details=“Alternate 1: Empty Colon Prefix (“Atsuo” / “Ruby”)”]
return {"my_first_var": my_first_var, "my_second_var": my_second_var}
# becomes
return {:my_first_var, :my_second_var}
I think this borrows from Ruby and its symbols. It’s [much?] less ambiguous than “Empty Colon Suffix”. That’s a good thing. It has already had a prototype implemented by Atsuo Ishimoto here. I guess my only critique would be that some syntax mistakes or typos in your object literal expression that would previously have been a syntax error, could now become unintentionally valid syntax.
I do like that syntaxes in this style do not introduce a new “type” of expression – at least at the tier of language hierarchy that dictionary literals sit at. You don’t need to learn all the rules for f-dict expressions, you just need to learn what :varname means inside a dict-like / set-like curly brace initializer. A reason to prefer f-dict is, perhaps, that the f-dict declares up front your intent to use “new syntax” and so linters and readers could perhaps be more prepared.
Discussion of Atsuo’s proposal is here:
[/details]
[details=“Alternate 2: Empty Equals (“PEP-736ish” / “kwargish”)”]
return {"my_first_var": my_first_var}
return {"my_first_var"=} # syntax error, probably
return {my_first_var=} # same as {"my_first_var": my_first_var}
# additionally, this could allow:
return {my_first_var="other value"} # same as {"my_first_var": "other value"}
The empty-equals syntax takes = from kwargs and PEP-736 and allows mixing it in with : elsewhere in the object initializer. It’s way less ambiguous than empty-colon, has cognitive underloading due to the similarity with kwargs, and it keeps the key name on the LHS of the expression (unlike Empty Colon Prefix), which I think is more natural to most readers.
[/details]
Alternate 3: Empty Colon Suffix
return {"my_first_var": my_first_var, "my_second_var": my_second_var}
# becomes
return {my_first_var:, my_second_var:}
The syntax is maybe “obvious” at first glance. You simply delete the right-hand-side of the “assignment” inside the object expression, remove the quotes, and you’re done, right? Unfortunately things get confusing when you try to hammer out the details of what to do with computed keys. Consider:
return {"my_first_var": my_first_var} # {'my_first_var': 'first val'}
return {"my_first_var":} # ???
return {my_first_var: my_first_var} # {'first val': 'first val'}
return {my_first_var:} # {'my_first_var': 'first val'}
It seems sort of unintuitive that the very similar code on lines 3 and 4 end up putting different keys in the dictionary.
Alternate 4: Empty Walrus
return {my_first_var:=, my_second_var: "myval", my_third_var:=}
I’m not sure why you would prefer this over “Empty Equals”, except that it retains the colon : from the traditional the object expression and combines it with = in the sense of the named-parameter calling convention. Unfortunately, to my mind, the := here doesn’t necessarily read like a single operator, it’s more like the : belongs to the object-literal syntax and indicates a binding of a key to a value, and the = is borrowed from kwargs syntax and indicates that the unquoted symbol will eventually be a string dict key, as in myfunc(a='v') may result in the string key "a" added to the callee’s kwargs dict. Also, := in this context doesn’t seem to share much in common with the side-effect assignment usage of :=.
Variation: Empty Left-Facing Walrus
Similar to the above, but =: does not overload the side-effect assignment := operator.
return {my_first_var=:, my_second_var: "myval", my_third_var=:}
sum(arry)
I’d like to hear your thoughts:
- “I think it would be great if Python had a similar shorthand for object initialization.”
- Do you agree, disagree, or something else?
- My proposal
- Do you have feedback on f-dict?
- How about on any of the alternatives?
- Do you have an alternative?