Skip to content

Use match statement in checkers (1) #10514

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
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

cdce8p
Copy link
Member

@cdce8p cdce8p commented Aug 22, 2025

After we added the first match statement in #10424 (comment), I checked a few other files to see which checks can be converted. There are actually quite a lot. Before we go ahead with it though, I think we should decide on some design choices and good practices first. I'll create comments for the specific sections. Feel free to add new ones as well in case I missed anything.

There are also mypy issues as astroid isn't fully typed yet. Opened PRs for it upstream already but those will be included in v1.18 at the earliest. Until then it might be best to just disable the offending error code(s) or add type ignores.

@cdce8p cdce8p added Maintenance Discussion or action around maintaining pylint or the dev workflow Skip news 🔇 This change does not require a changelog entry labels Aug 22, 2025
Copy link

codecov bot commented Aug 22, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.85%. Comparing base (4f98c18) to head (1996d57).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main   #10514      +/-   ##
==========================================
- Coverage   95.85%   95.85%   -0.01%     
==========================================
  Files         177      177              
  Lines       19198    19289      +91     
==========================================
+ Hits        18402    18489      +87     
- Misses        796      800       +4     
Files with missing lines Coverage Δ
pylint/checkers/match_statements_checker.py 100.00% <100.00%> (ø)
pylint/extensions/_check_docs_utils.py 93.72% <100.00%> (+0.01%) ⬆️
pylint/extensions/code_style.py 100.00% <100.00%> (ø)
pylint/extensions/for_any_all.py 100.00% <100.00%> (ø)
pylint/extensions/private_import.py 99.17% <100.00%> (+<0.01%) ⬆️

... and 8 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

🤖 According to the primer, this change has no effect on the checked open source code. 🤖🎉

This comment was generated for commit 1996d57

@@ -37,13 +37,13 @@ def visit_match(self, node: nodes.Match) -> None:
"""
for idx, case in enumerate(node.cases):
match case.pattern:
case nodes.MatchAs(pattern=None, name=nodes.AssignName()) if (
case nodes.MatchAs(pattern=None, name=nodes.AssignName(name=n)) if (
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we bind variables, what should the variable name be? A short-form like n here or better name? Maybe both is also an option?

Not sure where I picked this up from but using short-forms has somewhat grown on me. It's especially useful if we bind multiple variables in one case. Say nodes.Subscript(slice=s, value=v). Furthermore it might help reduce the ambiguity if the keyword name is used for the variable name as well, e.g. name=name. It also reminds me a bit of indices in loops with also just use i, j, so it might feel natural to look for the name in the match case.

Then again just name might still be better in the case body.

--
Another factor could be if it's just a generic name or a descriptive one. As example, one of the other example in this PR uses next_if_node.

args=case.pattern.name.name,
args=n,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though case.pattern.name.name is correctly narrowed by the match case, both pyright and mypy have problems inferring the type correctly. This can be resolved / worked around if the name is bound in the match case.

Note: Mypy will always infer it as Any since astroid is untyped, but even it it wasn't wouldn't be able to infer case .pattern.name.name probably at the moment.

Comment on lines -312 to +318
if len(node.orelse) == 1 and isinstance(node.orelse[0], nodes.If):
# elif block
next_if_node = node.orelse[0]
elif isinstance(next_sibling, nodes.If):
# separate if block
next_if_node = next_sibling
match (node.orelse, next_sibling):
case [[nodes.If() as next_if_node], _]:
# elif block
pass
case [_, nodes.If() as next_if_node]:
# separate if block
pass
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this one. It works 🤷🏻‍♂️ There are a few things to note though.

Only variable binding - no body
So far I've seen quite a few cases where it might make sense to "abuse" match to just do the variable binding. I considered using ... as body to make it more obvious that's what's happing but that doesn't look good / feels wrong. Maybe just pass is still the best option.

Performance
For the match to work here, I matched each case against a constructed sequence. The plain if-elif is probably a bit faster though I haven't profiled it yet. Ideally something like a match expression would be added to Python at some point. Something like

if node.orelse match [nodes.If() as next_if_node]:
    pass
elif isinstance(next_sibling, nodes.If):
    ...

Comment on lines +202 to +208
case nodes.Subscript(slice=s, value=v):
# slice is the next nested type
self._populate_type_annotations_annotation(s, all_used_type_annotations)
# value is the current type name: could be a Name or Attribute
return self._populate_type_annotations_annotation(
v, all_used_type_annotations
)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note the use of s and v here as I mentioned earlier. Would replacing s with slice / v with value be better?

Comment on lines +211 to +213
# TODO: remove once match false-positives are fixed, probably in v1.18
# https://github.com/python/mypy/pull/19708
"has-type",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mypy errors if we don't disable has-type:

pylint/extensions/private_import.py:199: error: Cannot determine type of "n"  [has-type]
pylint/extensions/private_import.py:200: error: Cannot determine type of "n"  [has-type]
pylint/extensions/private_import.py:201: error: Cannot determine type of "n"  [has-type]
pylint/extensions/private_import.py:201: note: Error code "has-type" not covered by "type: ignore" comment
pylint/extensions/private_import.py:204: error: Cannot determine type of "s"  [has-type]
pylint/extensions/private_import.py:207: error: Cannot determine type of "v"  [has-type]
pylint/extensions/private_import.py:213: error: Cannot determine type of "e"  [has-type]
pylint/checkers/match_statements_checker.py:46: error: Cannot determine type of "n"  [has-type]

Basically every time we do a variable binding inside a match case and use set variable.

@Pierre-Sassoulas
Copy link
Member

Love it, I like the elegance of match and it's proper in pylint with all the isinstance check we do all the time. I'm not sure if we should upgrade everything though. This kind of changes could tell us if our tests are actually covering or if we're just protected by years of live testing on real code bases and by not touching some part of the code without a good reason. Could be a lot of hassle if the latter is true and we go all in.

Regarding the standard I don't think we're at the step of making rules yet. Or at least I'm not : I'm not using match a lot. Seems like for a generic node n could be nice, like f for open and file. On other it would be nice to have a real name like "enumerate_start_kwargs" for clarity. Maybe think about it each time for now, use it for some time, then think about a rule later ?

@cdce8p
Copy link
Member Author

cdce8p commented Aug 22, 2025

Love it, I like the elegance of match and it's proper in pylint with all the isinstance check we do all the time.

Me too! I've used match once or twice before in the custom pylint plugin for HomeAssistant, so I knew it might work but I was honestly surprised at how good it actually works.

I'm not sure if we should upgrade everything though. This kind of changes could tell us if our tests are actually covering or if we're just protected by years of live testing on real code bases and by not touching some part of the code without a good reason. Could be a lot of hassle if the latter is true and we go all in.

True. So far I'd say the functional tests have caught most errors during conversion and helped a lot. There is always the risk that we introduce a regression though. The biggest issue I see though is that it will probably make backports much more difficult.

Regarding the standard I don't think we're at the step of making rules yet. Or at least I'm not : I'm not using match a lot. [...] Maybe think about it each time for now, use it for some time, then think about a rule later ?

Works for me. Just wanted to check what you guys think before doing more changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Maintenance Discussion or action around maintaining pylint or the dev workflow Skip news 🔇 This change does not require a changelog entry
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants