Dictionary and Function Argument Shorthand Syntax (JS ES6-style)

Hello Python Community,

I’d like to propose a new syntax feature that would reduce verbosity when constructing dictionaries and passing function arguments, inspired by JavaScript’s ES6 shorthand property syntax.

Motivation

Python developers frequently write code where variable names match dictionary keys or parameter names, resulting in redundant patterns:

Dictionary construction:

param1 = 10
param2 = 20
my_dict = {'param1': param1, 'param2': param2}  # repetitive

Function calls:

x = 10
y = 20

def my_func(x=None, y=None, z=5):
    print(x, y, z)

my_func(x=x, y=y)  # repetitive

This becomes particularly burdensome in data science workflows, configuration management, and API interactions where dozens of parameters are common.

Proposed Syntax

Allow variable names to automatically become dictionary keys when needed:

Dictionary shorthand:

param1 = 10
param2 = 20
my_dict = dict(param1, param2)
# Equivalent to: {'param1': param1, 'param2': param2}

Function argument shorthand:

a = 1
b = 2
param1 = 10
param2 = 20

def my_func(a, b, param1=None, param2=None):
    print(a, b, param1, param2)

my_func(a, b, **dict(param1, param2))
# Output: 1 2 10 20

Real-World Use Cases

This would significantly improve code clarity in scenarios like:

  • Machine learning: Parameter grids for sklearn’s GridSearchCV, model configurations

  • API clients: Request payloads where parameter names mirror local variables

  • Logging: Structured context with multiple variables

  • Configuration: Generating config dictionaries from local state

  • Database queries: Parameter binding with named placeholders

Example:

learning_rate = 0.01
batch_size = 32
epochs = 100

model.train(**dict(learning_rate, batch_size, epochs))

Instead of:

model.train(
    learning_rate= learning_rate,
    batch_size= batch_size,
    epochs= epochs
)

Benefits

  • Reduced verbosity: Less repetitive code without sacrificing clarity

  • Explicit references: Unlike locals() or vars(), you explicitly choose which variables to include

  • Modern alignment: Brings Python closer to contemporary language features (ES6, Rust)

  • Practical value: Especially beneficial for data-heavy and configuration-intensive applications

Backward Compatibility

This would be a purely additive change:

  • Existing dict() behavior with keyword arguments remains unchanged

  • Set literal syntax {param} is unaffected

  • No breaking changes to existing code

  • Clear distinction: dict(x=x) (current) vs dict(x) (proposed)

Potential Challenges

I recognize several areas requiring careful consideration:

  • Scope resolution: How to handle non-local variables or closures

  • Name conflicts: Behavior when variable names collide

  • Parser complexity: Implementation effort and maintenance burden

  • Python philosophy: Whether this aligns with “explicit is better than implicit”

Existing Workarounds and Their Limitations

Current approaches have drawbacks:

  • locals(): Captures all local variables (too broad, unpredictable)

  • vars(): Requires an object reference

  • Manual construction: Verbose (the problem we’re solving)

  • Dict comprehensions: Still require repetition: {k: v for k, v in [('x', x), ('y', y)]}

Questions for the Community

I’d greatly appreciate feedback on:

  1. Would this feature address pain points in your codebases?

  2. Are there implementation challenges I haven’t considered?

  3. Should we explore alternative syntax approaches?

  4. Are there similar past discussions or PEPs I should review?

Thank you for considering this proposal. I look forward to your insights.

Best regards,
Sadegh Khajepour

1 Like

Yes, you should look at PEP 736 – Shorthand syntax for keyword arguments at invocation.

11 Likes

Thanks for the reference — I’m aware of PEP 736 and I agree that the motivation (reducing x=x repetition) is similar. However, I think there is an important distinction between that proposal and what I’m suggesting here.

PEP 736 required changes to Python’s grammar and call syntax (e.g. allowing f(x=)), which directly impacts the parser and the core language semantics. That level of syntactic change is understandably high-risk and was one of the reasons it was ultimately rejected.

In contrast, this proposal does not introduce any new syntax and does not affect function call grammar at all. It is limited to a behavioral extension of dict() (i.e. an API-level change), where positional arguments would be interpreted as {name: value} pairs using existing name resolution rules.

So while the end goal is similar, the scope and impact are much smaller:

  • no grammar changes

  • no new parsing rules

  • no new syntax constructs

  • only a localized change to dict() semantics

Because of that, I believe this proposal should be evaluated separately from PEP 736 rather than treated as the same class of language change.

1 Like

It does magically make a call to dict different to every other function call in the entire Python language because dict somehow has to see the names of the variables being passed to it.

Does this work:

my_dict = dict
my_dict(var1, var2)

?

i.e. is it the name dict that’s special-cased or is it the dict function that has visibility into the caller?

5 Likes

That’s a fair question, and you’re right to frame it this way.

In the proposal as described, it would have to be the dict builtin itself that is special-cased, not the variable name dict. So yes, in your example:

my_dict = dict
my_dict(var1, var2)

this would behave exactly the same, because my_dict still refers to the builtin dict object.

That said, this does not require dict to “see variable names” in a magical way. Python already exposes this information at runtime via the frame object (inspect.currentframe()), and several existing features rely on similar mechanisms (e.g. error messages, debugging tools, dataclasses, namedtuple defaults, etc.).

So conceptually, this would be:

  • a builtin-level special case, not a language-wide rule

  • implemented in C for dict, not via general call semantics

  • no changes to how other functions receive arguments

However, I fully agree that this is still a semantic exception: dict() would behave differently from all other callables with respect to positional arguments. That trade-off is the real design question here — not whether it’s technically possible.

My point is mainly that this is not comparable to proposals like PEP 736, which required grammar changes and new call syntax. This stays within existing runtime capabilities, at the cost of introducing a narrowly scoped special behavior for one builtin.

Whether that trade-off is acceptable is debatable — but it’s a much smaller and more localized change than altering function call syntax across the language.

1 Like

If this doesn’t require compiler support (which is what I think you’re claiming) then why not simply write a helper function using normal Python to do what you want? If nothing else, such a function would be a valuable reference for the precise semantics you’re asking to be added.

Personally, I don’t think this can be done without compiler support, and as a result it’s unlikely to get accepted for the same reasons that PEP 736 was rejected - but if you believe otherwise, and can demonstrate it, then I’m happy to be proved wrong.

7 Likes

It can see that the caller has local variables namedparam1 and param2.
But it can’t see that the expressions that were evaluated to produce the
argument values consisted of those names, at least not without analysing
the bytecode, which seems like way too much trouble to go to.

But there’s an even bigger problem with this proposal – it’s ambiguous.
Currently this is valid code and produces a copy of the dictionary d1:

d1 = {‘answer’: 42}
d2 = dict(d1)

Under this proposal it would instead produce {‘d1’: {‘answer’: 42}}.

4 Likes

Thanks for the detailed feedback, Paul and Greg. I’d like to clarify a few points regarding the proposal.

  1. Regarding variable names visibility:
    Yes, the proposal relies on dict() being able to see that the caller has local variables with the given names. It does not require any “magical” access to the expressions used to produce the argument values — in other words, it cannot (and does not try to) know that x + y came from a variable x. Accessing the caller’s local frame, via mechanisms like inspect.currentframe(), is sufficient for the intended behavior.

  2. Helper function idea:
    I agree that a Python helper function could simulate parts of this behavior. That could serve as a reference for semantics, but it would have limitations, particularly in cases where we want the function to behave exactly like a dict constructor with positional arguments.

  3. Ambiguity / backward compatibility:
    Greg is correct that we must avoid changing the behavior of existing valid code. The proposal would need to clearly distinguish the “shorthand positional argument” case from the standard single-dict copy case to prevent ambiguity:

    d1 = {'answer': 42}
    d2 = dict(d1)  # must still produce {'answer': 42}, not {'d1': {...}}
    
    

    This can be done by only treating non-dict positional arguments as shorthand variable names, leaving existing dict-copy semantics unchanged.

  4. Scope and impact compared to PEP 736:
    Importantly, this proposal does not require grammar changes or compiler support. All changes are localized to the builtin dict(). Unlike PEP 736, it does not modify the call syntax or general function semantics — it’s a limited API-level enhancement.

In short, while the proposal does introduce a small semantic exception for dict(), it is far more localized and safer than language-wide syntax changes like PEP 736. With careful rules to preserve existing behavior, it can provide the convenience intended without affecting other functions or the parser.

To be perfectly honest, as you describe it here, it feels like there are way too many special rules and exceptions being made for the dict constructor, many of which seem quite difficult to explain clearly - the dict constructor already has a complex signature, and you’re suggesting that we extend it in a way that doesn’t match the rules for any other Python function.

At this point I’m a strong -1 on the whole proposal. I don’t think it adds aything like sufficient value. The existing syntax, dict(a=a, b=b, c=c) is slightly repetitive, I agree, but it’s easy to understand and follows the rules for every other function call.

You’re still not being clear here. Only parts of the behaviour? What bits can’t be “simulated”? You’re insisting the change needs no language change, while at the same time saying that what you want can’t be fully implemented with the language as it is. Which is it? Also, what do you mean by “simulated”? If the behaviour can be implemented as a Python function, that’s not a “simulation”, it’s an implementation of your proposal. You could even overwrite the dict builtin with your implementation of you wanted (which I’d strongly advise against, but it’s your choice).

3 Likes

I’d call this redundant. It’s unlikely param1 and param2 are used anywhere other than the definition of my_dict, so I don’t consider the direct use of 10 or 20 as magic constants anymore than they are in defining param1 or param2 in the first place.

my_dict = {
    'param1': 10,
    'param2': 20,
}

The function call isn’t repetitive if you keep in mind that there is no reason for argument expressions to be variables that happen to be the same as the parameter names. In general, the names in each scope will reflect different priorities and uses, and not be identical.

1 Like

Despite ChatGPT’s insistence that it doesn’t require compiler support, I am dubious that you’d be able to do it that way. Try implementing it, rather than just asking an AI whether this is possible. See what edge cases you haven’t figured out yet.

6 Likes

A reminder that posting LLM output, especially without disclosure, is explicitly against the rules of this site. Slightly broken English is preferable to LLM-filtered English - the latter makes it more difficult to see if you, the person, actually understand what we (and you) are saying.

10 Likes

I had similar DRY concerns when I was designing flufl.i18n, a library that provides higher level internationalization and translation services to Python applications. All of this work came from the Mailman project, which was one of the first fully i18n’d large Python applications at the time[1].

Without going into a huge amount of detail or history, I essentially did not want to have to write code that required the kind of varname=localvar boilerplate that you’re talking about here. Unlike I think here (or in PEP 736), there was vastly more of it because every single call site for translatable strings in the source required it.

flufl.i18n solves this by using sys._getframe(), another CPython feature for which Mailman was the impetus. Pulling all these bits together, now you only need to write _('$name lives in $place') and assuming name and place are local variables, the _() function will dig the values out at run time and do the proper substitutions. Here’s the code.

All that to say that I don’t personally feel like this (or PEP 736) are significant enough problems that the language needs to solve, and for really annoying cases, it’s likely you can write a library to do the magic for you, as was the case with flufl.i18n.


  1. Mailman translations was also the impetus for $-style template strings ↩︎

2 Likes

You could abuse t-strings as a moderately concise way of doing this:

def dict_from_tstring(t):
   return { i.expression: i.value for i in t.interpolations }

>>> a=5; b=10; c=20
>>> dict_from_tstring(t"{a}{b}{c}")
{'a': 5, 'b': 10, 'c': 20}

This is just presented as an off-hand thought rather than a serious recommendation. But it isn’t completely awful.

8 Likes

I don’t see how. Consider this: Even if it only implemented the new thing you’re proposing, it would still be very helpful as a proof-of-concept that it’s possible to pull off the above trick. Not just dict, but any Mapping object. Also any Sequence object, because dicts can also be constructed from (key, value) pairs. I don’t think that having all this magic in place of grammar changes makes in any more likely to be accepted. Explicit is better than implicit, and this is extremely implicit.

1 Like

Something similar would be entirely doable – pass the name of the variable as a string:

alpha = 1
beta = 2
d = dict('alpha', 'beta')

I’m still not much in favour of it, though. Why should dict() in particular be singled out for this rather special and non-obvious behaviour?

Also I’m not very comfortable with relying on frame inspection for this, which is an implementation detail of CPython that may not have an equivalent in other Python implementations. This is not so bad for a third party library, but having a core language feature depend on it is not really acceptable.

1 Like

Thanks everyone for the feedback! Looks like this kind of thing is better handled at the library level, so I’ll go down that path.

I actually found a library that does exactly what I wanted: dictvars. Check this out:

from dictvars import dictvars

user = "Alice"
form = "login"
comments = 5

dictvars(user, form, comments)
# Output: {'user': 'Alice', 'form': 'login', 'comments': 5}

Super handy if you want to turn local variables into a dictionary without typing all the names manually.

That library relies on reverse lookup of each object in the locals dict, which can be error-prone when there are multiple names referencing the same object, as noted in the library’s documentation.

One simple but robust approach that occurs to me is to use a lambda function that returns a tuple of variables:

def vardict(func):
    return dict(zip(func.__code__.co_names, func()))

a = 1
b = 2
print(vardict(lambda: (a, b))) # {'a': 1, 'b': 2}
1 Like

As @blhsing notes, the dict_vars library as suggested by @sakha1370 is not very reliable.

A different approach is suggested here, using the executing module. It does everything as suggested. Specifying a single positional argument that is a dict is handled as a special case.

Here’s the code and some samples:

import inspect
import executing


class vardict(dict):
    def __init__(cls, *args, **kwargs):
        if len(args) == 0:
            return super().__init__(kwargs)

        if len(args) == 1:
            try:
                return super().__init__(dict(args[0]) | kwargs)
            except Exception:
                ...

        call_frame = inspect.currentframe().f_back
        call_node = executing.Source.executing(call_frame).node
        if call_node is None:
            raise ValueError("could not assess variables")
        result = {}
        source = executing.Source.for_frame(call_frame)
        for node, right in zip(call_node.args, args):
            left = source.asttokens().get_text(node)
            if not left.isidentifier():
                raise ValueError(f"{left!r} not a valid identifier")
            if left in kwargs:
                raise ValueError(f"parameter {left!r} repeated")
            result[left] = right

        return super().__init__(result | kwargs)


one = 1
two = 2
numbers = vardict(one, two, three=3)
print(f"{numbers=}")
numbers = vardict({"one": 1, "two": 2}, three=3)
print(f"{numbers=}")
numbers12 = {"one": 1, "two": 2}
numbers = vardict(numbers12, three=3)
print(f"{numbers=}")
numbers3 = {"three": 3}
numbers = vardict(numbers12, numbers3)
print(f"{numbers=}")  # not what you would expect, maybe

s = "1234"
numbers = vardict(s, three=3)
print(f"{numbers=}")
numbers3 = {"three": 3}

s = ((1, 2), (3, 4))
numbers = vardict(s, three=3)
print(f"{numbers=}")
numbers3 = {"three": 3}

numbers = vardict(three=3)
print(f"{numbers=}")

Output:

numbers={'one': 1, 'two': 2, 'three': 3}
numbers={'one': 1, 'two': 2, 'three': 3}
numbers={'one': 1, 'two': 2, 'three': 3}
numbers={'numbers12': {'one': 1, 'two': 2}, 'numbers3': {'three': 3}}
numbers={'s': '1234', 'three': 3}
numbers={1: 2, 3: 4, 'three': 3}
numbers={'three': 3}