Title: Performance improvements for the MATCH_CLASS opcode · Issue #138912 · python/cpython · GitHub
Open Graph Title: Performance improvements for the MATCH_CLASS opcode · Issue #138912 · python/cpython
X Title: Performance improvements for the MATCH_CLASS opcode · Issue #138912 · python/cpython
Description: Feature or enhancement Proposal: For pylint we recently dropped support for Python 3.9 so I'm currently evaluating if we could replace some if statements with match. As most of the checks are similar to AST matching, the class pattern ma...
Open Graph Description: Feature or enhancement Proposal: For pylint we recently dropped support for Python 3.9 so I'm currently evaluating if we could replace some if statements with match. As most of the checks are simil...
X Description: Feature or enhancement Proposal: For pylint we recently dropped support for Python 3.9 so I'm currently evaluating if we could replace some if statements with match. As most of the checks are s...
Opengraph URL: https://github.com/python/cpython/issues/138912
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Performance improvements for the MATCH_CLASS opcode","articleBody":"# Feature or enhancement\n\n### Proposal:\n\nFor pylint we recently dropped support for Python 3.9 so I'm currently evaluating if we could replace some if statements with match. As most of the checks are similar to AST matching, the class pattern makes a lot of sense. I've noticed though that these are noticeable slower than the corresponding if statements. Yes, technically match does a few more checks which are just omitted in the if clauses but after looking at `_PyEval_MatchClass` I believe there is room for additional improvements.\n\n### Bare class pattern\nFirst let's compare a simple `isinstance` call with the equivalent class pattern (without any attributes).\n\n```py\nif isinstance(a, A):\n return True\nreturn False\n\n# ---\nmatch a:\n case A():\n return True\nreturn False\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eFull micro benchmark\u003c/summary\u003e\n\n```py\nfrom timeit import timeit\nsetup = \"\"\"\nclass A:\n def __init__(self, x, y):\n self.x = x\n self.y = y\na = A(1, 2)\n\ndef f1(a):\n if isinstance(a, A):\n return True\n return False\n\ndef f2(a):\n match a:\n case A():\n return True\n return False\n\"\"\"\nprint(f\"If\\t{timeit(\"f1(a)\", setup, number=10**7)}\")\nprint(f\"Match\\t{timeit(\"f2(a)\", setup, number=10**7)}\")\n```\n\u003c/details\u003e\n\n```\nBefore\nIf 0.29646458296338096\nMatch 0.6549855829798616\n\nAfter\nIf 0.2926184160169214\nMatch 0.3023994160466827\n```\n\nAt least on my machine the match statement is 2x slower. This could be avoided if we return early in case there are no arguments, thus skipping the creation of a list to track the attributes and set for the attribute names.\nhttps://github.com/python/cpython/blob/2d724939d7aea89b9473a18cfc096f94fcf39529/Python/ceval.c#L743-L752\n\n### One attribute\nOne of the most expensive parts of `MatchClass` is the set to check for duplicate attribute names. If there is only one attribute, this could be skipped entirely. An additional perf improvement can be archived if the attributes are placed into a tuple directly instead of creating an intermediary list and converting it to a tuple before returning it. That works since the match would fail if the argument count doesn't match.\n\n\u003cdetails\u003e\n\u003csummary\u003eFull micro benchmark\u003c/summary\u003e\n\n```py\nfrom timeit import timeit\nsetup = \"\"\"\nclass A:\n def __init__(self, x, y):\n self.x = x\n self.y = y\na = A(1, 2)\n\ndef f1(a):\n if isinstance(a, A) and a.x == 1:\n return True\n return False\n\ndef f2(a):\n match a:\n case A(x=1):\n return True\n return False\n\"\"\"\nprint(f\"If\\t{timeit(\"f1(a)\", setup, number=10**7)}\")\nprint(f\"Match\\t{timeit(\"f2(a)\", setup, number=10**7)}\")\n```\n\u003c/details\u003e\n\n```\nBefore\nIf 0.35557704203529283\nMatch 1.1494590829825029\n\nAfter\nIf 0.349508750019595\nMatch 0.6244700000388548\n```\n\n### Only keyword arguments\nThe check for duplicate attribute names is most helpful for mixed positional and keyword class patterns. Users would need to lookup `__match_args__` and make sure an attribute isn't accidentally specified twice. However, given the more than noticeable performance impact, I'd argue it could be skipped if there are only keywords given. The worst that would happen is that the match-case would always fail due to conflicting subpatterns. In that case however it would be fairly easy to debug as all keywords are specified directly. Furthermore linters will be able to detect these based on the AST alone.\n\n\u003cdetails\u003e\n\u003csummary\u003eFull micro benchmark\u003c/summary\u003e\n\n```py\nfrom timeit import timeit\nsetup = \"\"\"\nclass A:\n def __init__(self, x, y):\n self.x = x\n self.y = y\na = A(1, 2)\n\ndef f1(a):\n if isinstance(a, A) and a.x == 1 and a.y == 2:\n return True\n return False\n\ndef f2(a):\n match a:\n case A(x=1, y=2):\n return True\n return False\n\"\"\"\nprint(f\"If\\t{timeit(\"f1(a)\", setup, number=10**7)}\")\nprint(f\"Match\\t{timeit(\"f2(a)\", setup, number=10**7)}\")\n```\n\u003c/details\u003e\n\n```\nBefore\nIf 0.4226177910459228\nMatch 1.4355440840008669\n\nAfter\nIf 0.41212291695410386\nMatch 0.7927287500351667\n```\n\n--\nI'll be working on a PR for these.\n\n### Has this already been discussed elsewhere?\n\nThis is a minor feature, which does not need previous discussion elsewhere\n\n### Links to previous discussion of this feature:\n\n_No response_\n\n\u003c!-- gh-linked-prs --\u003e\n### Linked PRs\n* gh-138915\n* gh-139080\n* gh-144820\n\u003c!-- /gh-linked-prs --\u003e\n","author":{"url":"https://github.com/cdce8p","@type":"Person","name":"cdce8p"},"datePublished":"2025-09-15T11:05:52.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":3},"url":"https://github.com/138912/cpython/issues/138912"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:fc9a6cdb-96ad-7703-bea0-0e54825337e0 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | 8C6A:E7D9B:1D10B67:2685F30:6A51E12C |
| html-safe-nonce | f504aa94d2b8029892167ea03196acf673a9bfa2ea53dc29eefa85c1c7ffff0c |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI4QzZBOkU3RDlCOjFEMTBCNjc6MjY4NUYzMDo2QTUxRTEyQyIsInZpc2l0b3JfaWQiOiI2Mzk4MDcxMTUwNjY3Mjg3NDkiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | 2c4a095a47910c5cbc8252215c2f1843c773957229776917d8153f56e800ba0d |
| hovercard-subject-tag | issue:3417459533 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/python/cpython/138912/issue_layout |
| twitter:image | https://opengraph.githubassets.com/f64fe4446cb1f0fddcc4cc5fd8c24def56389e3f2c2a01f47f046b1870f87235/python/cpython/issues/138912 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/f64fe4446cb1f0fddcc4cc5fd8c24def56389e3f2c2a01f47f046b1870f87235/python/cpython/issues/138912 |
| og:image:alt | Feature or enhancement Proposal: For pylint we recently dropped support for Python 3.9 so I'm currently evaluating if we could replace some if statements with match. As most of the checks are simil... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | cdce8p |
| hostname | github.com |
| expected-hostname | github.com |
| None | b9a586c06a05a7a86fc7e3f4dbd03e42f6869085879aa184aa6369456dbd50fb |
| turbo-cache-control | no-preview |
| go-import | github.com/python/cpython git https://github.com/python/cpython.git |
| octolytics-dimension-user_id | 1525981 |
| octolytics-dimension-user_login | python |
| octolytics-dimension-repository_id | 81598961 |
| octolytics-dimension-repository_nwo | python/cpython |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 81598961 |
| octolytics-dimension-repository_network_root_nwo | python/cpython |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 7aed05249554b889eb33d002851a973eebcc7e91 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width