Shorthand dict literal initialization

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:

  1. Many prefer using the { } syntax for initializing objects over the dict() syntax:
    It would be nice to be able to continue use { } while still benefiting from the spirit of PEP-736.

  2. 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:

  1. Don’t use an f-dict
  2. Add the computed key after the initializer (d[runtime_key] = 'whatever').
  3. Unpack the computed key from a {} dict into the f-dict
d = f{my_first_var, **{my_second_var: "custom value"}}
  1. 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:

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:
Shorthand notation of dict literal and function call

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.

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:

  1. “I think it would be great if Python had a similar shorthand for object initialization.”
    • Do you agree, disagree, or something else?
  2. My proposal
    • Do you have feedback on f-dict?
    • How about on any of the alternatives?
    • Do you have an alternative?
2 Likes

You can join the existing discussion:

I don’t like f{a, b, c} because that could be the syntax for a frozenset.

7 Likes

How often do people have a use for string-keyed dictionaries, each value in which is exactly the same as its key? Granted they can be mutated afterwards.

Even where such a dict is needed or will be subsequently mutated, and where a set will not suffice, is a simple dictionary comprehension really so bad?

{x: x for x in [my_first_var, my_second_var, my_third_var]}

You’re not wrong Mike, that this can initialize some objects, as in Python everything is an object. But it would help people to understand your proposal if you clarify that this only for dicts, not overriding __init__ on object instances of every class.

2 Likes

I ran some numbers:

Number of py files processed 69,464
Total bytes processed 1019.38 MB
Total DRY matches 2499
Total non-DRY matches 78255
DRY / non-DRY % 3.19%

A bit underwhelming, but still. :​) Also, I didn’t count dictionaries where all keys were DRY except say, just one, which I think would have been fairer. But I got tired of poking at it.

Nice!

Thank you for helping with the terminology. I changed the title of the post.


Purely by accident – while doing some of that text processing – I ran across this from Vim’s :help Dictionary:

To avoid having to put quotes around every key the #{} form can be used in legacy script. This does require the key to consist only of ASCII letters, digits, ‘-’ and ‘_’. Example: :let mydict = #{zero: 0, one_key: 1, two-key: 2}

[otherwise it would be: :let mydict = {"zero": 0, "one_key": 1, "two-key": 2}].

I think I’m becoming more partial to Atsuo’s way. It seems less invasive.

Wow - well done.

Which source of files did you run that on? It’s not a high percentage. But clearly some people are still writing that.

Another alternative that is nice and mutable is to use .get with the default instead of [key]. Items with the same value as their key will be returned the same as:

{}.get(key, key)

Mike, you know in Javascript, the object shorthand notation uses existing variables from the scope? Where as this idea only supports values in dicts that are strings?

Also, before f-strings, Python also had strings, or ‘strings’, beginning with r, b, and in Python 2, u (raw strings, bytes literals, and unicode strings). f-strings when they came in, already fitted in, and didn’t change the language’s readability in such a fundamental way as f-dicts would. And they were a breath of fresh air compared to the previous options (.format(), "%s" % and templates).

Moreover, as Chris Markiewicz pointed out, the fundamental question is not how to parse the f{} syntax. It’s how should the key names therein be parsed, especially if they could also be interpreted as an expression, e.g. be the same name as another variable in the scope. In which case the behaviour would be very different to that of Object literals, possibly naively expected by a programmer coming from Javascript?

f{my_first_var, my_second_var, 1+1}

x = 1
f{x, y, z}

f-dicts will not just add the f. They add baggage - a new context in which string literals no longer have to be quoted, and in which the unquoted name of a variable, does not refer to that variable.

The shorthand function call is gaining a bit of traction. It’s reasonable to assume that if f-dicts are accepted, then the keyword arg shorthand proposal will have been accepted.

In that case, is solving the above key parsing issue really worth saving typing three characters, and avoiding a few trailing =s?

To be fair, that baggage already exists in the import statement. It would be new to an expression, though.

The problem any such shorthand solves can be thought of as a special case of two more general problems:

  1. The ability to reify the effective namespace (all scopes combined) in the same way globals reifies the global namespace and locals reifies the local namespace.

  2. The ability to define a dict slice (a new dict with a subset of the key/value pairs from another)

The latter could be solved simply by adding a new slice instance method to the mapping protocol. The former is trickier; I don’t seen any good solution other than to add a new name to the built-in scope, breaking backwards compatibility. An awkward compromise would be to add a new dunder name, so that you could write something like

__namespace__.slice("my_first_var", "my_second_var", "my_third_var")

which is not terribly longer than the proposed f-dict syntax

f{my_first_var, my_second_var, my_third_var}

and can’t be easily mistaken for a set display.

(I realize actually implementing __namespace__ efficiently would probably not be trivial.)

1 Like
  1. The ability to define a dict slice (a new dict with a subset of the key/value pairs from another)

This can also be handled via:

{k: another[k] for k in subset}
1 Like

Yes, but since we’re on the subject of shorthand, I wanted to keep the boilerplate to a minimum. Conceptually, obtaining a slice only requires a dict and a list of keys, not an explanation of how the keys are used to get the associated values and construct a new dict with them.

1 Like

I agree. Introducing a new unseen convention (and mini-scope) for a minor shorthand is probably overkill. I am not overly attached to f-dict. I sort of knew this all along, but I wanted to prompt discussion! As I mentioned earlier, I’m probably moving over into Atsuo’s camp.

Yep! In fact, my original grep pattern included r?b?u?f?['\"]. But, I took out the prefixes from the regex to make it easier to work on the regex.

{x, 1+1} is a syntax error in JavaScript as f{x, 1+1} would also be a syntax error in Python. Both for the same reason: the shorthand property (SPN) name must be an IDENTCHAR+ in both languages. In Python’s case that should mean the SPN needs to be compatible with the PEG parser’s treatment of the LHS of a kwarg-style call: myfn(LHS=RHS).

A conceptual goal was that {shorthand, property} code from JavaScript could largely just be copy-pasted as f{shorthand, property} into Python and work in almost all the same cases, and raise errors in the same cases. Not so much because I am a big fan of JavaScript, but rather that its notation did (and continues to) influence JSON, and JSON is important broadly. JSON5 has: “Object keys may be an ECMAScript 5.1 IdentifierName.”, although of course it doesn’t have shorthand, because it doesn’t have variables.

Not to get too sidetracked with JS, but you would need to use {[1+1]: "two"} in JS in order to get Python’s {1+1: "two"}. In f-dict, neither f{1+1: "two"} nor f{[1+1]: "two"} would be legal. You would have to use {}. f-dict is restrictive in order to not be too confusing.

I agree. I don’t think something invasive f-dict would be accepted prior to shorthand function call. I expect shorthand function call to pass eventually.

In a sense, yes. Because if the kwarg-shorthand gets passed, I will start seeing this (to paraphrase one of Atsuo’s examples) everywhere:

def register_user(first, last, addr1, addr2):
    return dict(first=,
                last=,
                addr1=,
                addr2=)

Whereas before I would read:

def register_user(first, last, addr1, addr2):
    return {'first': first,
            'last': last,
            'addr1': addr1,
            'addr2': addr2}

{} is just historical/traditional in a Python sense also in a polyglot sense of PHP, Ruby, C, C++, Perl, JS/TS, TOML, JSON, C# all supporting similar {} init constructs.

Alas, all proposals for shorthand get the same critique: people point out how small the savings is. Most widely used programming languages are already fairly terse, and so if we naively applied this rule, we would never get sugar at all. So we must consider a larger scope than just keystrokes saved (even though billions of people typing gigabytes of Python every year, for decades, will add up.) We also consider readability, maintainability, and keeping up with conventions (cf. lambdas, map(), flatMap(), etc). An easy (but maybe not high quality) proxy for measuring these factors is watch whether other popular/similar languages have implemented similar shorthands and whether those shorthands have been well-received by developers and accepted by style checkers.

Rust, of all things, has a local_var → struct_field shorthand:

    struct Person {first: String, last: String}
    let first = String::from("Mike");
    let last = String::from("C");
    let person = Person { first, last };
1 Like

Interesting. I’d like to think more about that! More convenience operators on dict would not make me unhappy at all.

One of the things I like about any of the barestring shorthand approaches (Atsuo’s proposal, f-dict, PEP 736), is that it’ll make it easier for refactoring tools (IDEs, etc.) to track “renaming” a local variable that is bound by such a shorthand. This allows a variable rename to cause a different key to be produced into the new dict-literal. Since the intent-of-the-intent of the shorthand is to allow the developer to express their intent to maintain a variable whose name is tightly bound to whatever the key-protocol of the produced dict is, I think this would very often be desirable behavior.


I have started thinking about something like the following, which could be used to implement an f-dict convention (if someone wanted it), or any number of other things. It would be a more powerful version of tagged templates, and could maybe thought of as a stepping stone past f"{expr=}".

class BraceCallable:
    def __brace_call__(self, exprs):
        for expr in exprs:
            lhs = expr.lhs
            rhs = expr.rhs
            print(f"LHS: '{lhs.expr_str}' = {type(lhs.val)}:{lhs.val}")
            print(f"RHS: '{rhs.expr_str}' = {type(rhs.val)}:{rhs.val}")
            print(f"LHS==RHS: {lhs == rhs}")
        return {} # whatever you like

# 't' for template
t = BraceCallable()

v1 = "X1"; v2 = "X2"; i1 = 1; i2 = 2 # some variables and values to play with
# and a hypothetically syntactically valid example:

o = t{1+1, 1+1="2", v1, v1=v2, v1=, v1:v2, i1+i2, i1=i2, i1=, i1:i2}

# Broken down by print output:
## t{1+1} # attr shorthand
# LHS: '1+1' = int:2
# RHS: '1+1' = int:2
# LHS==RHS: False

## t{1+1="2"}
# LHS: '1+1' = int:2
# RHS: '"2"' = str:2
# LHS==RHS: False

## t{v1} # attr shorthand
# LHS: 'v1' = str:X1
# RHS: 'v1' = str:X1
# LHS==RHS: True

## t{v1=v2} # kwarg-ish
# LHS: 'v1' = str:v1
# RHS: 'v2' = str:X2
# LHS==RHS: False

## t{v1=} # kwarg-ish # (LHS == RHS) == True
# LHS: 'v1' = str:v1
# RHS: 'v2' = str:X1
# LHS==RHS: True

## t{v1:v2} # dict-ish
# LHS: 'v1' = str:X1
# RHS: 'v2' = str:X2
# LHS==RHS: False

## t{i1+i2} # attr shorthand
# LHS: 'i1+i2' = int:3
# RHS: 'i1+i2' = int:3
# LHS==RHS: True

## t{i1=i2} # kwarg-ish
# LHS: 'i1' = int:1
# RHS: 'i2' = int:2
# LHS==RHS: False

## t{i1=} # kwarg-ish # (LHS == RHS) == True
# LHS: 'i1' = int:1
# RHS: 'i1' = int:1
# LHS==RHS: True

## t{i1:i2} # dict-ish
# LHS: 'i1' = int:1
# RHS: 'i2' = int:2
# LHS==RHS: False

The runtime overhead might seem unbearable. But consider that the infrastructure is probably mostly there ever since f-string’s =} was implemented:

print(f"{1+1=}")            # 1+2=2
print(f"{f'{1+1=}'=}")      # f'{1+1=}'='1+1=2'
print(f"{eval('1+1')=}")    # eval('1+1')=2

Other overhead might be a permissive grammar for brace_mapping_pattern (to follow the naming convention of mapping_pattern in cpython/Grammar/python.gram).

Also a concern is that f"{any_py_expr=}" is (I think?) kind of viewed as a niche debugging tool, whereas people might start using __brace_call__ for “important” things.