Dict Unpacking in Python

4 days ago (github.com)

Lame: Submit a PEP, campaign for community support, write a patch, go back and forth with the maintainers, endure weeks and months of bikeshedding, then maybe, eventually have your feature included in the next Python release.

Game: Use the codec hack, immediately publish your feature for all Python versions, then write "please do not use" to be safe.

While not nearly as fun as the OP, I’d note that this sort of unpacking is very pleasant in the newish PEP636 match case statements:

https://peps.python.org/pep-0636/#matching-builtin-classes

  • Looks really cool!

    Will this allow combinations of bound and unbound variables?

    E.g.:

      def is_on_horizontal_line(point, line_y):
        match point:
          case (x, line_y):
            return f"Yes, with x={x}"
          case _:
            return "No"
    

    Seems both useful and potentially confusing.

    • It allows you to use bound variables/constants as long as the expression includes a dot so you can distinguish it from a capture variable.

      Scala allows matching against bound variables but requires it either start with an uppercase letter or be surrounded in backtics in the pattern. I don't know that that would make sense for python, but there could potentially be some special syntax to spicify you want to compare against an existing variable instead of capturing a new variable.

      1 reply →

Did not know that such things could be accomplished by registering a new file coding format. Reminds me of https://pypi.org/project/goto-statement/

  • This one is arguably even more of a hack; it's working at the source code level rather than the AST level.

    The "coding" here is a bytes-to-text encoding. The Python lexer expects to see character data; you get to insert arbitrary code to convert the bytes to characters (or just use existing schemes the implement standards like UTF-8).

    • > it's working at the source code level rather than the AST level.

      this (lexing) is the only use of the codec hack - if you want to manipulate the AST you do not need this and can just to `ast.parse` and then recompile the function.

      1 reply →

  • I think there's a package to treat Jupyter notebooks as source code (so you can import them as modules).

    While the OP package is obviously a joke, the one with notebooks is kind of useful. And, of course, obligatory quote about how languages that don't have meta-programming at the design level will reinvent it, but poorly.

    • I'd argue "import from notebooks" is still only helpful in the "space bar heating" sense.

      I think Notebooks are great for quick, "explorative" sketches of code. They are absolutely terrible for organizing "production" code.

      I know it often happens that something starts in a notebook and then sort of morphs into a generic script or full-on application. But I think, this is usually the signal you should refactor, pull out the "grown" parts from the notebooks and organize them into proper Python modules.

      If you have parts that are still experimental or explorative, consider importing your new modules into the notebook instead of the other way around.

      Source: personal experience

I found dictionary unpacking to be quite useful, when you don't want to mutate things. Code like:

    new_dict = {**old_dict, **update_keys_and_values_dict}

Or even complexer:

    new_dict = {
        **old_dict,
        **{
            key: val
            for key, val in update_keys_and_values_dict
            if key not in some_other_dict
        }
    }

It is quite flexible.

I use the Python package 'sorcery' [0] in all my production services.

It gives dict unpacking but also a shorthand dict creation like this:

    from sorcery import dict_of, unpack_keys
    a, b = unpack_keys({'a': 1, 'b': 42})
    assert a == 1
    assert b == 42
    assert dict_of(a, b) == {'a': 1, 'b': 42}

[0] https://github.com/alexmojaki/sorcery

In short, it runs a text preprocessor as the source text decoder (like you would decode from Latin-1 or Shift-JIS to Unicode).

After using JS, Python dicts and objects feel so cumbersome. I don't see why they need to be separate things, and why you can't access a dict like `dict.key`. Destructuring is the icing on the cake. In JS, it even handles the named args use case like

   const foo = ({name, age, email}) => { }

I'm guessing all of this has been proposed in Python before, and rejected in part because at this point it'd create way more confusion than it's worth.

Coming from lisp/haskell I always wanted destructuring but after using it quite a lot in ES6/Typescript, I found it's not always as ergonomic and readable as I thought.

Anthony is also the maintainer of the deadsnake ppa, if you were searching for reasons to love him more.

  • Believe he’s the same person who won’t allow pyflakes to support # noqa, because it’s “opinionated.”

    As if dropping that word is some sort of justification. I don’t know what the opinion is! Worse is better?

The confusing bit to me is that the LHS of this

{greeting, thing} = dct

is a set, which is not ordered, so why would greeting and thing be assigned in the order in which they appear?

  • I don't think they are. They are matched by variable names, so this:

      {thing, greeting} = dct
    

    Should have the exact same result.

This confuses me a bit

  dct = {'a': [1, 2, 3]}
  {'a': [1, *rest]} = dct
  print(rest)  # [2, 3]

Does this mean that i can use?

  dct = {'a': [1, 2, 3]}
  {'b': [4, *rest]} = dct
  print(rest)  # [2, 3]

and more explicit

  dct = {'a': [1, 2, 3]}
  {'_': [_, *rest]} = dct
  print(rest)  # [2, 3]

  • > Does this mean that i can use?

    They'll both trigger a runtime error, since the key you're using in the pattern (LHS) does not match any key in the dict.

    Note that `'_'` is an actual string, and thus key, it's not any sort of wildcard. Using a bare `_` as key yields a syntax error, I assume because it's too ambiguous for the author to want to support it.

I would donate $500 to the PSF tomorrow if they added this, the lack of it is daily pain

  def u(**kwargs):
    return tuple(kwargs.values())

Am I missing something, is this effectively the same?

*I realize the tuple can be omitted here

  • You have to pull them out by key name, and not just get everything. Here's a working version, though with a totally different syntax (to avoid having to list the keys twice, once as keys and once as resulting variable names):

      >>> def u(locals, dct, keys):
      ...     for k in keys:
      ...         locals[k] = dct[k]
      ... 
      >>> dct = {'greeting': 'hello', 'thing': 'world', 'farewell': 'bye'}
      >>> u(locals(), dct, ['greeting', 'thing'])
      >>> greeting
      'hello'
      >>> thing
      'world'
      >>> farewell
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
      NameError: name 'farewell' is not defined
    
    

    Modifying locals() is generally frowned upon, as there's no guarantee it'll work. But it does for this example.

  • Or use itemgetter:

      >>> from operator import itemgetter
      >>> dct = {'greeting': 'hello', 'thing': 'world', 'farewell': 'bye'}
      >>> thing, greeting = itemgetter("thing", "greeting")(dct)
      >>> thing
      'world'
      >>> greeting
      'hello'