Skip to content

fixtures: use a better data structure for storing FixtureDefs - #14984

Open
bluetech wants to merge 5 commits into
pytest-dev:mainfrom
bluetech:arg2node2fixturedef
Open

fixtures: use a better data structure for storing FixtureDefs#14984
bluetech wants to merge 5 commits into
pytest-dev:mainfrom
bluetech:arg2node2fixturedef

Conversation

@bluetech

@bluetech bluetech commented Sep 7, 2026

Copy link
Copy Markdown
Member

This PR fixes #14942.

The commits

The 1st and 2nd commits are related internal documentation fixes.

The 3rd commit is a little refactor to encapsulate access to FixtureManager._arg2fixturedef to make the next commit clearer.

The 4th commit is main change, I reproduce the commit message below.

The 5th commit adds backward compat for string nodeids in register_fixture. I split it from the previous commit to make it easier to review the main change without the ugly compat. I will squash it before merging.

Description

Previously, FixtureManager stored the registered FixtureDefs in _arg2fixturedefs which is

<fixutre name> -> [FixtureDef]

where the FixtureDefs are ordered by visibility.

There are two inefficiencies with this:

  1. When registering a fixture, we need to find the appropriate index to insert in the list. This is done with slow quadratic is_visibility_more_specific checks.

    Before 7186cd4, FixtureDefs were always appended, relying on the collection order, so there was no quadratic issue. But then we added pytest.register_fixture which is not guaranteed to be called in collection order.

    This is Test collection is 10x slower on 9.1.1 compared to 9.0.3 #14942, introduced in v9.1.0.

  2. When looking up FixtureDefs for a node, the entire list needed to be filtered for visibility to the node (_matchfactories). With many fixtures registered with the same name (even if completely unrelated), this can be slow.

    This is an old issue.

Change the way we store the FixtureDefs to _arg2node2fixturedefs, which is

<fixture name> -> (Node -> [FixtureDef])

i.e. instead of storing the FixtureDefs for a name in a single big list, store them by the Node under which they are registered.

This fixes (1) since now just need to append to arg2fixture2nodes[name][node].

Fixes (2) since no longer need to filter a big list (scaling with number of fixtures registered with same name). Instead need to look up the FixtureDefs registered for the node and its ancestors (scales with height of the collection tree, which should be OK).

Performance

Performance numbers with reproducer from #14942 (comment) (NCLASSES=10000 time pytest reproducer.py):

  • This PR: 11.0s
  • main: 35.0s
  • Commit before 7186cd4: 16.7s

It's still kinda slow, but a profile shows that's for other preexisting reasons. The improvement over main is due to fixing problem (1), and over before the regression due to fixing problem (2).

Backward compat

This breaks plugins which directly access FixtureManager._arg2fixturedefs (double private 😀). From my local corpus (678 plugins), I see it done in these plugins:

  • pytest-bdd - Moderately elaborate usage. I will try to send a PR to fix if this is merged.
  • pytest-deadfixtures - Just iterates over all FixtureDefs. I will send them a PR if this merged.
  • python-pytest-steps - getting some downloads but hasn't been updated in last 5 years. I will ask smarie about it if this is merged.
  • pytest-airflow - not popular
  • pytest-eucalyptus - not popular
  • pytest-exploratory - not popular
  • pytest-missing-fixtures - not popular
  • pytest-tipsi-django - not popular

I think this is acceptable.

This is an internal function, so it's not a official guarantee, but
let's explicitly document the behavior internally at least.
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Sep 7, 2026
…ide the class

It's easier to handle this way.
Previously, FixtureManager stored the registered FixtureDefs in
`_arg2fixturedefs` which is
    <fixutre name> -> [FixtureDef]
where the FixtureDefs are ordered by visibility.

There are two inefficiencies with this:
1. When registering a fixture, we need to find the appropriate index to
   insert in the list. This is done with slow quadratic
   `is_visibility_more_specific` checks.

   Before 7186cd4, FixtureDefs were
   always appended, relying on the collection order, so there was no
   quadratic issue. But then we added `pytest.register_fixture` which is
   not guaranteed to be called in collection order.

   This is pytest-dev#14942, introduced in v9.1.0.

2. When looking up FixtureDefs for a node, the entire list needed to be
   filtered for visibility to the node (`_matchfactories`). With many
   fixtures registered with the same name (even if completely
   unrelated), this can be slow.

   This is an old issue.

Change the way we store the FixtureDefs to `_arg2node2fixturedefs`,
which is
    <fixture name> -> (Node -> [FixtureDef])
i.e. instead of storing the FixtureDefs for a name in a single big list,
store them by the Node under which they are registered.

This fixes (1) since now just need to append to
`arg2fixture2nodes[name][node]`.

Fixes (2) since no longer need to filter a big list (scaling with number
of fixtures registered with same name). Instead need to look up the
FixtureDefs registered for the node and its ancestors (scales with
height of the collection tree, which should be OK).

Fix pytest-dev#14942
@bluetech
bluetech force-pushed the arg2node2fixturedef branch from 690d7d4 to d938a56 Compare September 8, 2026 10:13

@nicoddemus nicoddemus 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.

Great work!

@@ -0,0 +1,2 @@
Fixed a regression in 9.1.0 that made collection very slow (quadratic) when many fixtures are defined with the same name.
Then can particularly happen when many tests are defined in separate classes which inherit from a base class which defines fixtures (the fixtures are repeated per class).

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
Then can particularly happen when many tests are defined in separate classes which inherit from a base class which defines fixtures (the fixtures are repeated per class).
This can happen particularly when many tests are defined in separate classes which inherit from a base class which defines fixtures (the fixtures are repeated per class).

Comment thread src/_pytest/fixtures.py
Comment on lines +2393 to +2396
for fixturedef in [
*node2fixturedefs.get(parent, ()),
*nodeid2fixturedefs.get(parent.nodeid, ()),
]

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.

We could use itertools.chain instead of building a list, might be a bit more efficient (or maybe not that much, given the list of fixturedefs is much smaller now).

Comment thread src/_pytest/fixtures.py
# Legacy fallback, for plugins still using the deprecated nodeid-based
# API without a node reference.
# Part of FIXTURE_NODEID_DEPRECATED deprecation.
self._arg2nodeid2fixturedefs: Final[

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.

Instead of a separate datastructure, is it possible for us to get the nodeid from the session collection tree? Or perhaps the node might not exist at this point yet?

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.

Test collection is 10x slower on 9.1.1 compared to 9.0.3

2 participants