It’s a usability issue; mappings are used quite differently than sequences.
Compare to class patterns rather than sequence patterns.

On Sat, Nov 14, 2020 at 22:04 David Foster <davidf...@gmail.com> wrote:

>  From PEP 636 (Structural Pattern Matching):
>  > Mapping patterns: {"bandwidth": b, "latency": l} captures the
> "bandwidth" and "latency" values from a dict. Unlike sequence patterns,
> extra keys are ignored.
>
> It surprises me that ignoring extra keys would be the *default*
> behavior. This seems unsafe. Extra keys I would think would be best
> treated as suspicious by default.
>
> * Ignoring extra keys loses data silently. In the current proposal:
>
>      point = {'x': 1, 'y': 2, 'z': 3)
>      match point:
>          case {'x': x, 'y': y}:  # MATCHES, losing z  O_O
>              pass
>          case {'x': x, 'y': y, 'z': z}:  # will never match  O_O
>              pass
>
> * Ignoring extra keys is inconsistent with the handling of sequences: We
> don't allow extra items when using a destructuring assignment to a
> sequence:
>
>      p = [1, 2]
>      [x, y] = p
>      [x, y, z] = p  # ERROR: ValueError: not enough values to unpack
> (expected 3, got 2)  :)
>
> * Ignoring extra keys in mapping patterns is inconsistent with the
> current proposal for how sequence patterns match data:
>
>      point = [1, 2, 3]
>      match point:
>          case [x, y]:  # notices extra value and does NOT match  :)
>              pass
>          case [x, y, z]:  # matches :)
>              pass
>
> * Ignoring extra keys is inconsistent with TypedDict's default "total"
> matching behavior:
>
>      from typing import TypedDict
>
>      class Point2D(TypedDict):
>          x: int
>          y: int
>
>      p1: Point2D = {'x': 1, 'y': 2}
>      p2: Point2D = {'x': 1, 'y': 2, 'z': 3)  # ERROR: Extra key 'z' for
> TypedDict "Point2D"  :)
>
> * It is *possible* to force an exact key match with a pattern guard but
> it's clumsy to do so.
>    It should not be clumsy to parse strictly.
>
>      point = {'x': 1, 'y': 2, 'z': 3)
>      match point:
>          # notices extra value and does NOT match, but requires ugly
> guard :/
>          case {'x': x, 'y': y, **rest} if rest == {}:
>              pass
>          case {'x': x, 'y': y, 'z': z, **rest} if rest == {}:
>              pass
>
>
> To avoid the above problems, **I'd advocate for disallowing extra keys
> in mapping patterns by default**. For cases where extra keys want to be
> specifically allowed and ignored, I propose allowing a **_ wildcard.
>
>
> Some examples that illustrate behavior when *disallowing* extra keys in
> mapping patterns:
>
> 1. Strict parsing
>
>      from typing import TypedDict, Union
>
>      Point2D = TypedDict('Point2D', {'x': int, 'y': int})
>      Point3D = TypedDict('Point3D', {'x': int, 'y': int, 'z': int})
>
>      def parse_point(point_json: dict) -> Union[Point2D, Point3D]:
>          match point_json:
>              case {'x': int(x), 'y': int(y)}:
>                  return Point2D({'x': x, 'y': y})
>              case {'x': int(x), 'y': int(y), 'z': int(z)}:
>                  return Point3D({'x': x, 'y': y, 'z': z})
>              case _:
>                  raise ValueError(f'not a valid point: {point_json!r}')
>
> 2. Loose parsing, discarding unknown data.
>     Common when reading JSON-like data when it's not necessary to output
> it again later.
>
>      from typing import TypedDict
>
>      TodoItem_ReadOnly = TypedDict('TodoItem_ReadOnly', {'title': str,
> 'completed': bool})
>
>      def parse_todo_item(todo_item_json: Dict) -> TodoItem_ReadOnly:
>          match todo_item_json:
>              case {'title': str(title), 'completed': bool(completed), **_}:
>                  return TodoItem_ReadOnly({'title': title, 'completed':
> completed})
>              case _:
>                  raise ValueError()
>
>      input = {'title': 'Buy groceries', 'completed': True,
> 'assigned_to': ['me']}
>      print(parse_todo_item(input))  # prints: {'title': 'Buy groceries',
> 'completed': True}
>
> 3. Loose parsing, preserving unknown data.
>     Common when parsing JSON-like data when it needs to be round-tripped
> and output again later.
>
>      from typing import Any, Dict, TypedDict
>
>      TodoItem_ReadWrite = TypedDict('TodoItem_ReadWrite', {'title': str,
> 'completed': bool, 'extra': Dict[str, Any]})
>
>      def parse_todo_item(todo_item_json: Dict) -> TodoItem_ReadWrite:
>          match todo_item_json:
>              case {'title': str(title), 'completed': bool(completed),
> **extra}:
>                  return TodoItem_ReadWrite({'title': title, 'completed':
> completed, 'extra': extra})
>              case _:
>                  raise ValueError()
>
>      def format_todo_item(item: TodoItem_ReadWrite) -> Dict:
>          return {'title': item['title'], 'completed': item['completed'],
> **item['extra']}
>
>      input = {'title': 'Buy groceries', 'completed': True,
> 'assigned_to': ['me']}
>      output = format_todo_item(parse_todo_item(input))
>      print(output)  # prints: {'title': 'Buy groceries', 'completed':
> True, 'assigned_to': ['me']}
>
>
> Comments?
>
> --
> David Foster | Seattle, WA, USA
> Contributor to TypedDict support for mypy
> _______________________________________________
> Python-ideas mailing list -- python-ideas@python.org
> To unsubscribe send an email to python-ideas-le...@python.org
> https://mail.python.org/mailman3/lists/python-ideas.python.org/
> Message archived at
> https://mail.python.org/archives/list/python-ideas@python.org/message/ZPUVT7AF67VKNLSSGUHOBIM5F46ZEE77/
> Code of Conduct: http://python.org/psf/codeofconduct/
>
-- 
--Guido (mobile)
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/OBXREKQ2TKLAB4OGSY2QOZWQTP2KLUHA/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to