PEP 845: Leading-Dot Value Patterns for match statement

Continuing the discussion from PEP 845: Leading-Dot Value Patterns:

Please chime in with your thoughts!

6 Likes

I assume you’ve done lots of bikeshedding on the choice of punctuation already, so all I’ll say is that I would never guess that’s what the . meant without reading the definition (multiple times), and would confidently guess it did something different.

Virtually any other symbol would give me pause, rather than assuming wrong. Even a double dot would throw me enough to go “I need to look this up”.

A single dot with no context implies to me that the context is thing we’re matching on, rather than locals.

7 Likes

Reading the original PEP and discussion again it seems what has not really been discussed is the trailing dot. But I can assume that something like case foo.: would be probably harder to read than the leading dot.

And I suppose that matching to an attribute of the thing itself being matched is a rarer use case that can already be addressed by pulling the entire thing to be matched out to a local variable , and then doing a plain old attribute lookup pattern on it.

I’m curious, what would you guess it did?

Something along the lines of book.author, and then I’d have to figure out what that would actually mean as a match constraint. One of those things that at first glance seems reasonable, and very quickly appears to be nonsense, but now I have to unwind a whole lot of thinking to go “oh, it actually means ==author”.

7 Likes

Why not make it so you have to specify the target when matching against a variable?

def get_book_titles_for_author(author: str):
    for book in BOOKS:
        match book:
            case {"title": title, "author": author as author}:
                yield title

This is currently a syntax error, so it’s up for grabs. I feel this is (perhaps slightly) better than inventing another meaning for yet another Python symbol.

Edit: Ah, but of course, a as b is valid today and assigns the value to both a and b, so there’s no hope of detecting typos. I don’t know what the usefulness of a as b might be, so perhaps it should be deprecated regardless.

The issue is interpreting author as a value instead of author as an identifier, right? Why not just convert it into a value?

def get_book_titles_for_author(author: str):
    for book in BOOKS:
        match book:
            case {"title": title, "author": author + ""}:
                yield title

This is currently a syntax error, but I think it would be clear enough that you want the value of the expression to be used as the “value pattern”.

Obviously, for numbers you’d need to add zero, and lists you’d add a empty list, and for a tuple you’d add an empty tuple, and for … Hmm, ok, maybe we don’t want to actually add anything; we just want it to be interpreted an expression instead of an identifier.

def get_book_titles_for_author(author: str):
    for book in BOOKS:
        match book:
            case {"title": title, "author": (author)}:
                yield title

There we go; it is an expression!

2 Likes

This syntax with (author) is already valid and interpreted the same as the bare name, so that would be a backwards incompatible change.

And str(author) doesn’t work, 'cause that is just another deconstruction pattern that binds a string to author.

@stoneleaf I felt similarly to @steve.dower. I had to reread the PEP abstract several times to tease out the meaning. This sentence “This makes it possible to match local variables and global variables defined in the same module without using a guard clause.” seems to me to be the compelling reason for the PEP.

The “Leading-Dot” in the PEP name adds more confusion to me than the desire to match without using a guard. It may be helpful to reword the abstract and perhaps add a second example to illustrate the global and local value match with the guard (current) and without the guard (this PEP).

== might actually be a good syntax for this.

def get_book_titles_for_author(author: str):
    for book in BOOKS:
        match book:
            case {"title": title, "author": ==author}:
                yield title

(You could even extend it with similar syntax for other compare ops like !=, though I’d struggle to see the use.)

10 Likes

Besides having to use guard clauses there’s also another workaround. The standard library currently provides no way for programmatic chained local+nonlocal+global+builtin name lookup, but this can be emulated:

lazy import inspect
class VariableLookup:
    """Provides dot lookup for local, nonlocal, global variables and builtins.
    Relies on interpreter live stack frame support. 
    """
    __slots__ = ()
    def __getattr__(self, name: str, /):
        frame = inspect.currentframe()
        depth = 0
        while frame is not None:
            if depth > 0:
                local = frame.f_locals
                try:
                    return local[name]
                except KeyError:
                    # We encounter the module frame, stop here
                    if local == frame.f_globals:
                        try:
                            return frame.f_builtins[name]
                        except KeyError:
                            raise NameError(f"name {name!r} is not defined", name=name) from None
            depth += 1
            frame = frame.f_back
        # Either there's no frame support from the interpreter 
        # or we got incomplete frames
        raise NotImplementedError("no or incomplete stack frame support")

Variables = VariableLookup()

If only locals and globals are concerned, one can use Variables = types.SimpleNamespace(globals() | locals()) at each match site.

Then we can simply use the existing value lookup pattern:

a = "a"
b = "b"
def fn(foo):
    match foo:
        case Variables.a:
            print("matches a")
        case Variables.b:
            print("matches b")
fn("a") # matches a

If variable value patterns (leading dot or otherwise) are not deemed useful enough to warrant syntax changes (40% of respondents surveyed in the PEP were fine with the status quo) I think the above (obviously will need to be implemented in C) will be the next best option. Granted, these are not the best-looking workarounds, but I don’t think Scope-qualified lookups should be completely rejected off-hand.

2 Likes

This looks OK in a dict pattern, but I wonder would this would look like in an object pattern, would it result in triple equal sign then? Or maybe it collapses to a double one there as well.

case Book(author===author):

vs

case Book(author==author):

I could see valid arguments for both, but I think the underlying question is if this should be thought of as a prefix operator or not. Would it be valid to do this?

case Book(author=(==author)):
4 Likes

Good point. case Book(author===author): would be the logically consistent answer, but (1) that looks terrible and (2) it tokenizes as == followed by =, not the reverse. Parenthesizing it with case Book(author=(==author)) seems like the best solution, but I’d definitely count this as an argument against the == syntax.

3 Likes

I sympathize. It took a while for me to internalize that Python’s with was nothing like Foxpro’s with. Considering the use of the dot in match statements currently, I would hope that the leading dot would be a gentle learning curve.

I’m personally dubious of syntax proposals that don’t use a dot, considering matching with variables in namespaces requires no extra syntax.

Structural pattern matching is also quite intricate, and users will have to learn its syntax by reading the documentation either way. Considering this, I think users would understand the leading dot syntax quite well in context.

In any case, I’m very happy to see this PEP since variable matching always felt like an afterthought to me while using structural pattern matching.

4 Likes

I think the ..author option could be worth considering further.

In from .config import setting, there’s an implied __package__ to the left of the dot (kind of). It’s also an import statement, so we already know we’re out of the realm of normal name lookups.

The implied namespace lookup in .author is a lot weaker, since it’s not actually reflecting the namespace.attr lookup pattern.

By contrast, ..author does follow the standard value pattern syntax, with the first . being a standin for “do the value lookup via regular name resolution”, and the second being the regular attribute lookup notation.

3 Likes