Skip to content

Clarify the relation between marks and keywords - #15011

Open
RonnyPfannschmidt wants to merge 7 commits into
pytest-dev:mainfrom
RonnyPfannschmidt:marks-keywords-4569
Open

Clarify the relation between marks and keywords#15011
RonnyPfannschmidt wants to merge 7 commits into
pytest-dev:mainfrom
RonnyPfannschmidt:marks-keywords-4569

Conversation

@RonnyPfannschmidt

Copy link
Copy Markdown
Member

I prompted an agent (Claude Opus 5 in Claude Code) to work through #4569. The
investigation, the commits and this description are its work; I read it and I
am posting it.


#4569 asks to revisit the relation between marks and keywords. Mapping what
that relation actually is came first, and it changes the shape of the issue:

-k does not read item.keywords, and has not since 2.4.0 (#306).
KeywordMatcher.from_item builds its own name set from the node chain,
extra_keyword_matches, the test function's __dict__ and iter_markers().
node.keywords is a separate mapping, fed by different code, read only by
request.keywords, TestReport.keywords and the terminal's skip folding.

So the two mark-to-keyword hacks from #4564 and #4649 are independent of one
another, and the # todo: this is a hell of a hack comment pointing at #4569
sits on the one that does not influence selection at all. That makes most of
#4569 a documentation and coverage problem rather than a refactoring one.

One commit per item:

Behaviour

  • -k no longer matches attributes that land in a test function's __dict__
    without anyone meaning them as keywords. -k wrapped selected every
    functools.wraps-decorated test and -k pytestmark every directly marked
    one. Private and dunder names are skipped, plus a named set. The legacy
    test_func.slow = True idiom keeps working.
  • add_marker stores a Mark in node.keywords instead of a
    MarkDecorator, matching what collection-time marks store.

Documentation

  • The -k help text described the pre-5.4 implementation ("a Python evaluable
    expression"), limited matching to test names and parent classes, and never
    mentioned markers — the thing The-Compiler's comment on revisit relation between marks and keywords #4569 asks for. It
    now lists the real keyword sources and contrasts with -m.
  • markers.rst framed -k as name-only and corrected itself only in a
    trailing paragraph; usage.rst, which the option reference links to, still
    described eval semantics.
  • KeywordMatcher, Function's keywords parameter, TestReport.keywords,
    Node.keywords and FixtureRequest.keywords docstrings were stale or wrong.
  • The --runslow and incremental examples use get_closest_marker rather than
    "name" in item.keywords.

Tests

Pins -k matching marks from module/class/base classes, -k ignoring marker
arguments, -k seeing a mark added by a conftest collection hook,
extra_keyword_matches on an item, and writing into item.keywords having no
effect on -k or -m — despite the 2.3.4 changelog claiming it integrates
with -m. Also enables the -k half of test_mark_expressions_no_smear,
commented out since the marker transfer was removed, and pins that -m is
case sensitive.

Deliberately left open

Whether the function.__dict__ scan should exist at all is the remaining
design question in #4569. Dropping it means deprecating attribute-marks and
finding another discriminator for the skip folding in terminal.py, which
keys off "pytestmark" in report.keywords. Unifying KeywordMatcher with
node.keywords depends on that answer, so neither is attempted here.

🤖 Generated with Claude Code

RonnyPfannschmidt and others added 7 commits September 13, 2026 10:05
The -k matcher scans a test function's __dict__ to support the legacy
`test_fn.foo = True` keyword idiom, but that scan also picked up
attributes nobody meant as keywords: pytest's own `pytestmark` storage,
and the bookkeeping decorators leave behind (`__wrapped__` and the rest
of what functools.wraps copies, lru_cache's `cache_parameters`).

So `-k wrapped` selected every wraps-decorated test and `-k pytestmark`
selected every directly marked one.

Skip private and dunder names, plus a named set of known attributes.

Part of pytest-dev#4569.

Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
Marks applied during collection are stored in node.keywords as Mark
objects; add_marker stored the MarkDecorator instead, so the value type
depended on how the mark got there.

Nothing inside pytest reads keyword values, and both types expose
name/args/kwargs, so downstream `item.keywords["x"].args` keeps working.

Part of pytest-dev#4569.

Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
The -k help text still described the pre-5.4 implementation ("a Python
evaluable expression"), claimed matching is limited to test names and
their parent classes, and never mentioned that marker names are
keywords - which is the behaviour people trip over in pytest-dev#4569.

Replace it with the real list of keyword sources, and state the
substring/case-insensitive contrast with -m in both option blocks.

Part of pytest-dev#4569.

Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
KeywordMatcher claimed it only matches Class and Function names, which
stopped being true in 2.4.0; Function's `keywords` parameter claimed it
feeds "-k" matching, which it does not; TestReport.keywords promised a
name -> value mapping while the values are all 1.

Also state what Node.keywords is for, and that -k does not read it.

Part of pytest-dev#4569.

Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
The markers.rst section title and intro presented -k as matching test
names, in contrast to -m matching markers, and only corrected itself in
a trailing paragraph after three console dumps. usage.rst, the page the
option reference links to, still described the pre-5.4 eval semantics
and listed only filenames, classes and functions.

Lead with what a keyword is in both places, and say plainly that marker
names are keywords.

Part of pytest-dev#4569.

Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
The --runslow and incremental examples tested for a marker with
`"name" in item.keywords`, which is also true when a parent node happens
to be named that, or when the test function carries an attribute of that
name. These snippets are widely copied, so they are where the idiom of
treating keywords as a mark lookup keeps coming from.

Part of pytest-dev#4569.

Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
Several long-standing behaviours had no test: -k matching marks from the
module, class and base classes; -k ignoring marker arguments; -k seeing
a mark added by a conftest collection hook; extra_keyword_matches on an
item rather than a collector; and writing into item.keywords having no
effect on either -k or -m, despite the 2.3.4 changelog claiming it
"integrates with the -m option".

Also enable the -k half of test_mark_expressions_no_smear, commented out
since the marker transfer that made marks smear onto a shared base class
was removed, and pin that -m is case sensitive.

Part of pytest-dev#4569.

Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Sep 13, 2026

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Didn't see anything outrageous but I'm not sure I'm knowledgable enough to do this review.

@bluetech bluetech left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, look like good changes to me.

Comment on lines +180 to +182
Because marker names are keywords, ``-k http`` below selects tests marked
``@pytest.mark.http`` just as well as tests merely named ``test_send_http``.
Use :option:`-m` when you want markers and nothing else.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This paragraph is a little confusing since the example doesn't use @pytest.mark.http. I understand you intend to say "if hypothetically a test were marked with @pytest.mark.http then it would also be selected` but it doesn't quite read this way.

I think either omit the paragraph or integrate it into the example.

which implements a substring match on the test's *keywords*, instead of the
exact match on markers that :option:`-m` provides.

The keywords of a test are its own name, the names of the test's parents

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I figure the reason why this paragraph was previously at the end was a desire to not inundate the reader with details before giving the example. Which I kinda agree with, though both ways are OK.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would change this to a listing, seems easier to parse/make sense of it, and include examples:

The keywords of a test are:

* Its own name (e.g. `test_foo`).
* The name of the test's parents (usually the name of the file and class it is in) (e.g. `TestFoo`, `test_foo.py`)
* ...

Comment thread changelog/4569.doc.rst
@@ -0,0 +1 @@
The documentation of :option:`-k` now lists what a test's keywords actually are, including marker names, and says how ``-k`` differs from :option:`-m`. The examples that looked up a marker now use :meth:`Node.get_closest_marker <_pytest.nodes.Node.get_closest_marker>` instead of ``item.keywords``.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It would be nice to link to said documentation.

Does it refer to example/markers.rst? If so, it did explain what "keywords" are. So I think there first sentence here can be removed.

Comment thread changelog/4569.bugfix.rst
@@ -0,0 +1 @@
``-k`` no longer matches attributes that end up in a test function's ``__dict__`` without being meant as keywords, such as pytest's own ``pytestmark`` storage and the bookkeeping left behind by :func:`functools.wraps` and :func:`functools.lru_cache`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
``-k`` no longer matches attributes that end up in a test function's ``__dict__`` without being meant as keywords, such as pytest's own ``pytestmark`` storage and the bookkeeping left behind by :func:`functools.wraps` and :func:`functools.lru_cache`.
:option:`-k` no longer matches attributes that end up in a test function's ``__dict__`` without being meant as keywords, such as pytest's own ``pytestmark`` attribute and the bookkeeping left behind by :func:`functools.wraps` and :func:`functools.lru_cache`.

Comment thread doc/en/how-to/usage.rst
The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``.
Use ``""`` instead of ``''`` in expression when running this on Windows

The keywords of a test are its own name, the names of the file, class and directories it is in,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

usage.rst is one of the "started" documentation pages, so I think it would be nice to keep it relatively short. Maybe can replace this paragraph with a hyperlink to example/markers.rst in the above paragraph?


Marker names are matched exactly and case-sensitively, and only markers are matched:
unlike :option:`-k`, ``-m`` never matches test, class, module or directory names.
Marker keyword arguments can be matched as well, as in ``pytest -m "device(serial='123')"``.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would move the example to a line in Examples above.

@dataclasses.dataclass
class KeywordMatcher:
"""A matcher for keywords.
"""A matcher for keywords, used by ``-k``.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IMO the low level KeywordMatcher shouldn't refer to high level -k argument. Though maybe it makes things clearer.

Would at least change to "as used by -k".

Comment on lines -161 to -162
Only matches names of items which are either a :class:`Class` or a
:class:`Function`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems this sentence was incorrect leftover from 60906f7.

Comment thread src/_pytest/fixtures.py
Comment on lines +639 to +640
This method is useful if you don't want to have the marker on all
function invocations.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(Preexisting) I wonder what "function invocation" refers to here? I can't make sense of what it's trying to say, maybe refers to parametrization?

Comment thread src/_pytest/reports.py
#: A name -> value dictionary containing all keywords and
#: markers associated with a test invocation.
#: The names in :attr:`Node.keywords <_pytest.nodes.Node.keywords>`
#: of the item, each mapping to ``1``; the values of the node keywords

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we document the 1 then it would be good to change the type annotation to Literal[1] to statically guarantee it.

which implements a substring match on the test's *keywords*, instead of the
exact match on markers that :option:`-m` provides.

The keywords of a test are its own name, the names of the test's parents

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would change this to a listing, seems easier to parse/make sense of it, and include examples:

The keywords of a test are:

* Its own name (e.g. `test_foo`).
* The name of the test's parents (usually the name of the file and class it is in) (e.g. `TestFoo`, `test_foo.py`)
* ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants