From 93cc2eaea3bd50110eb517c779a7e1c28b63844b Mon Sep 17 00:00:00 2001 From: Thomas MK Date: Fri, 17 Jul 2026 14:55:45 +0200 Subject: [PATCH 1/4] PEP 845: Leading-Dot Value Patterns --- peps/pep-0845.rst | 359 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 peps/pep-0845.rst diff --git a/peps/pep-0845.rst b/peps/pep-0845.rst new file mode 100644 index 00000000000..0107173e2ed --- /dev/null +++ b/peps/pep-0845.rst @@ -0,0 +1,359 @@ +PEP: 845 +Title: Leading-Dot Value Patterns +Author: Thomas Kehrenberg , Marc Mueller +Sponsor: Ethan Furman +Discussions-To: Pending +Status: Draft +Type: Standards Track +Created: 25-Aug-2026 +Python-Version: 3.16 + + +Abstract +======== + +This PEP enables the use of unqualified names as **value patterns** in match +statements. A name prefixed with a dot is looked up using the normal name +resolution rules and compared to the match subject by equality in the same +manner as existing value patterns, instead of being treated as a **capture +pattern**. This makes it possible to match local variables and global variables +defined in the same module without using a guard clause. + +.. code-block:: python + + def get_book_titles_for_author(author: str): + for book in BOOKS: + match book: + case {"title": title, "author": .author}: + yield title + + +Motivation +========== + +When the match statement was proposed in :pep:`634`, the decision was made to +only support dotted names, i.e. attributes, in **value patterns** because it is +not possible to differentiate simple (undotted) names from **capture +patterns**. However, this decision makes it especially difficult to match +something against local variables, e.g. function arguments. The current +workaround for this limitation is to combine a name capture pattern with an +explicit guard clause. + +.. code-block:: python + + BOOKS: dict[str, str] + + def get_book_titles_for_author(author: str): + for book in BOOKS: + match book: + case {"title": title, "author": book_author} if ( + book_author == author + ): + yield title + +While this works, it is unnecessarily difficult to read and write, especially +if the match case gets more complex. Furthermore, it has some additional +limitations: + +- As a workaround, it might not get taught together with the value pattern. In + particular developers new to the match statement might find it difficult to + come up with it at first. + +- The guard clause is separate from the **value pattern**. For deeply nested + patterns, this increases the complexity while reading the match case. It is + necessary to keep track of all **capture patterns** mentally just for it to + be used in the guard clause. At which point it is not obvious whether or not + the name is also used in the case body as well. + +- The name being checked is often closely related or even the same as the + variable. In the example above both are ``author``. This makes it necessary + to choose a different, often suboptimal name for the name capture only to + avoid accidentally overwriting the variable. + +- Due to the similar names, it is also frequently not possible to know only by + reading the guard clause which is the name and which the variable being + checked. + +- Combining the workaround with ``OR`` patterns is limited because ``OR`` + patterns require that name captures are defined in **all** alternatives. This + can make it necessary to duplicate the case body if an alternative does not + need the name capture. + +- The guard clause is only checked after the pattern itself matches. Especially + for complex patterns, this can lead to unnecessary work when the name capture + is followed by other patterns. As the capture always succeeds, the other + patterns are evaluated even if it is obvious to the outside observer that the + guard will fail. + +This PEP picks up on a deferred suggestion from :pep:`635` to use a leading dot +for **value patterns** with simple (undotted) names. The example above could +then be written as: + +.. code-block:: python + + def get_book_titles_for_author(author: str): + for book in BOOKS: + match book: + case {"title": title, "author": .author}: + yield title + + +Matching global variables +------------------------- + +A similar issue exists when trying to match a subject against global variables, +in particular those defined in the same module. While other workaround exist to +be able to use the attribute syntax --- for example the variable could be moved +to another module or be wrapped by an enum or namespace --- it is often desired +to keep existing code as is when using a match statement. + +This might especially be the case for sentinels added in :pep:`661`. Sentinels +are likely to be used in some way in the same module they are defined in, but +with the current syntax it is not possible to match against them without using +workarounds like the guard clause. + +Specification +============= + +The value pattern will be extended to support simple names, besides attributes, +if they are prefixed by a leading dot. The lookup is performed following the +standard Python name resolution rules. + +Grammar +------- + +The pattern grammar of :pep:`634` is extended. The ``value_pattern`` rule +gains a second alternative: + +.. code-block:: peg + + value_pattern: + | attr !('.' | '(' | '=') + | '.' NAME !('.' | '(' | '=') + +and the key of a mapping pattern item may likewise be a leading-dot name: + +.. code-block:: peg + + key_value_pattern: + | (literal_expr | attr | '.' NAME) ':' pattern + +The new form consists of exactly one dot followed by exactly one identifier. +Attribute chains after a leading dot (``.ns.CONST``) are not permitted. + + +Rationale +========= + +Why a leading dot +----------------- + +The existing rule is: "**a value pattern containing a dot is a lookup; a name +pattern without a dot is a binding**". The leading-dot form extends this rule +to an unqualified name, for which the portion to the left of the dot is empty: + ++-------------------+----------------------+ +| Pattern | Meaning | ++===================+======================+ +| ``Color.RED`` | lookup (status quo) | ++-------------------+----------------------+ +| ``ui.colors.RED`` | lookup (status quo) | ++-------------------+----------------------+ +| ``.RED`` | lookup (new) | ++-------------------+----------------------+ +| ``red`` | binding (status quo) | ++-------------------+----------------------+ + +Under this proposal, the rule can be stated as: "**a dot anywhere in a name +pattern means lookup**". The proposal does not add a keyword or operator, and +it does not change the meaning of any existing pattern. + +This rule was considered in :pep:`635` but was deferred as no consensus could +be reached at the time and it could always be added later without +backward-compatibility issues. The predecessor PEP for pattern matching +:pep:`622` used this rule. + +In a `poll accompanying the discussion thread for this PEP +`__, respondents could approve of +multiple options. The majority (71%) preferred the leading-dot syntax over the +alternatives (discussed below) and over the status quo. + +Visibility of the dot +--------------------- + +One concern noted in :pep:`635` was that the dot "would not be a visible-enough +marker". We disagree. + +We believe that the ease of teaching and using the rule outweighs the concerns +about visibility. Alternatives such as the guard clause workaround are often +more difficult to read, in particular in more complex match cases where +the guard clause is separate from the value pattern. + +Furthermore, Python already uses a leading dot in relative imports: +``from .config import DEFAULTS`` and ``from config import +DEFAULTS`` differ only by the dot, and both forms are valid. + +Additionally, syntax highlighters could distinguish capture patterns from value +patterns and make the difference between ``NAME`` and ``.NAME`` more visible. + +Other languages, like Swift, also use leading dots in pattern matching. + + +Backwards Compatibility +======================= + +The change is fully backwards compatible. So far using ``.name`` raised a +``SyntaxError``. + + +Security Implications +===================== + +There are no new security implications from this proposal. + + +How to Teach This +================= + +The rule presented in the :pep:`636` tutorial can be stated as follows: + + In a pattern, a name **with a dot** is looked up and compared; a name + **without a dot** captures the subject. + +The leading-dot form can be introduced as "a value pattern whose namespace part +is empty": you write ``helpers.MISSING`` when the constant lives in a separate +namespace and ``.MISSING`` when it does not. + +.. code-block:: python + + MISSING = sentinel('MISSING') + + match value: + case .MISSING: # value pattern; looked up and compared + ... + case found: # capture pattern; always matches and binds + ... + +Documentation for the ``match`` statement will be updated to include the +leading-dot syntax. + + +Reference Implementation +======================== + +None yet. + + +Rejected Ideas +============== + +Other sigils +------------ + +Alternative one-character or operator-like markers were proposed in the +original discussions and again in the thread for this PEP: ``^CONSTANT`` (the +"pin" operator, as in Elixir), ``$CONSTANT``, ``?CONSTANT``, ``==CONSTANT``, +``{CONSTANT}``, and backticks. None of these markers is currently used for +lookup in Python patterns. By contrast, a dot is already part of every dotted +value pattern. Curly braces could be confused with mapping patterns, while +``==CONSTANT`` could imply support for other comparison operators. General +comparison patterns are outside the scope of this PEP, as discussed below. In +the community poll referenced in `Why a leading dot`_, each of these options +received fewer approvals than the leading-dot form. + +Keyword-based markers (``value NAME``, ``constant case NAME:``) +--------------------------------------------------------------- + +Spellings such as ``case value MISSING:`` or a modified ``constant case +MISSING:`` clause are more visible than a dot, which was their primary +advantage in the discussion. These forms would add new soft keywords or +keyword-like syntax. A pattern-level form such as ``case Node(kind=value +LEAF):`` is less concise when nested, while a clause-level form cannot mark one +subpattern within a larger pattern. + +Scope-qualified lookups (``global.NAME``, ``nonlocal.NAME``) +------------------------------------------------------------ + +Reusing the ``global`` and ``nonlocal`` keywords as pseudo-namespaces would +make the scope of the lookup explicit. It would also couple each pattern to +the scope in which the constant is defined. For example, a module-level +constant would use ``global.NAME``, but moving it into an enclosing function +would require changing its patterns to ``nonlocal.NAME``. These forms do not +cover local names or builtins. The proposed extension ``local.NAME`` would +require a new keyword because ``local`` is currently an ordinary identifier. +Standard name resolution covers all of these scopes without additional syntax. +In addition, ``nonlocal.NAME`` does not have an equivalent expression form +elsewhere in Python. + +Distinguishing by case of the name +---------------------------------- + +Treating ``UPPER_CASE`` names as constants was considered and rejected during +the original pattern-matching design: no other part of core Python attaches +semantics to the case of an identifier, and identifiers in scripts without a +case distinction (e.g. CJK characters) could never be matched as values. + +Allowing attribute chains after the leading dot +----------------------------------------------- + +``.ns.CONST`` would be exactly equivalent to ``ns.CONST``, providing a second +spelling for existing syntax without adding a capability. Restricting the new +form to a single identifier avoids this duplication. + +Special-casing sentinels only +----------------------------- + +Since :pep:`661` gives each sentinel a distinct type, matching could be +supported through class patterns, or sentinels could be special-cased as +quasi-literals like ``None``. But the problem is not specific to sentinels: +any unqualified constant (a numeric constant, an interned default object, an +enum member imported with ``from module import MEMBER``) has the same issue. +Solving it for one kind of value would leave other unqualified constants +unsupported and would add a sentinel-specific exception to the pattern grammar. + +Making lookup the default and marking captures instead +------------------------------------------------------ + +Revisiting the fundamental :pep:`634` decision that a bare name is a capture +pattern would be a breaking change and is therefore rejected. + +Restricting the leading-dot syntax to the current scope +------------------------------------------------------- + +A leading dot in relative imports means "relative to the current package", and +some participants in the discussion noted that ``case .NAME:`` would similarly +suggest "in the current namespace" and could therefore imply that names in +outer scopes are excluded. In this proposal, the dot does not select a scope. +The name after the dot is resolved as it would be in an ordinary expression at +that location (local, enclosing, global, then builtin scope). The dot +determines only whether the pattern performs a lookup or a binding. We believe +this is the more useful behavior and that it is easier to teach and understand +than a scope-restricted lookup. + +Resolution in the builtin scope also permits matching values that are otherwise +available only as bare names. ``NotImplemented`` and ``Ellipsis`` are constants +that --- unlike ``None``, ``True`` and ``False`` --- are ordinary names rather +than keywords. They consequently have capture semantics when used as bare +patterns and cannot be qualified without importing ``builtins``. The same +applies to builtin types when the type object itself is the value being +matched, for example when dispatching on a type stored in an annotation or +configuration value: + +.. code-block:: python + + match target_type: + case .int | .float: + return NumericColumn(target_type) + case .str: + return TextColumn() + +Note the difference from the class pattern ``case int():``, which matches +*instances* of ``int``: the value pattern ``case .int:`` matches the type +object itself. + + +Copyright +========= + +This document is placed in the public domain or under the CC0-1.0-Universal +license, whichever is more permissive. From 161d82e44d12075d707c84eb7de643f61bbf8fc0 Mon Sep 17 00:00:00 2001 From: Thomas M Kehrenberg Date: Tue, 25 Aug 2026 13:14:06 +0200 Subject: [PATCH 2/4] Add code owners --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7707fccd25a..4a8ebf19e32 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -719,6 +719,7 @@ peps/pep-0841.rst @corona10 @sobolevn peps/pep-0842.rst @ZeroIntensity peps/pep-0843.rst @ZeroIntensity peps/pep-0844.rst @warsaw +peps/pep-0845.rst @tmke8 @cdce8p # ... peps/pep-2026.rst @hugovk # ... From e89d4ef6bc6150d2dfacb57977d705cd5e0e8d6c Mon Sep 17 00:00:00 2001 From: Thomas M Kehrenberg Date: Tue, 25 Aug 2026 13:25:48 +0200 Subject: [PATCH 3/4] Update .github/CODEOWNERS Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4a8ebf19e32..e914553577c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -719,7 +719,7 @@ peps/pep-0841.rst @corona10 @sobolevn peps/pep-0842.rst @ZeroIntensity peps/pep-0843.rst @ZeroIntensity peps/pep-0844.rst @warsaw -peps/pep-0845.rst @tmke8 @cdce8p +peps/pep-0845.rst @ethanfurman # ... peps/pep-2026.rst @hugovk # ... From a8bcc38fdabae24ac43413e6a192245037b4cef2 Mon Sep 17 00:00:00 2001 From: Thomas M Kehrenberg Date: Tue, 25 Aug 2026 18:34:40 +0200 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- peps/pep-0845.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/peps/pep-0845.rst b/peps/pep-0845.rst index 0107173e2ed..de2c059d2c1 100644 --- a/peps/pep-0845.rst +++ b/peps/pep-0845.rst @@ -41,7 +41,7 @@ explicit guard clause. .. code-block:: python - BOOKS: dict[str, str] + BOOKS: list[dict[str, str]] def get_book_titles_for_author(author: str): for book in BOOKS: @@ -241,7 +241,9 @@ leading-dot syntax. Reference Implementation ======================== -None yet. +A reference implementation is available at +https://github.com/cdce8p/cpython/tree/pep845-match-leading-dot. +A online demo can be tested at https://pep845-demo.pages.dev/. Rejected Ideas