-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
PEP 845: Leading-Dot Value Patterns #5107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tmke8
wants to merge
4
commits into
python:main
Choose a base branch
from
tmke8:pep-845
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+362
−0
Open
Changes from 3 commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,359 @@ | ||
| PEP: 845 | ||
| Title: Leading-Dot Value Patterns | ||
| Author: Thomas Kehrenberg <tmke8@posteo.net>, Marc Mueller | ||
| Sponsor: Ethan Furman <ethan@stoneleaf.us> | ||
| 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 | ||
| <https://discuss.python.org/t/107916/61>`__, 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. | ||
|
tmke8 marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| 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. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.