diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 25280994687..9243007b697 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,26 +1,23 @@
 name: deploy
 
 on:
-  push:
-    tags:
-      # These tags are protected, see:
-      # https://github.com/pytest-dev/pytest/settings/tag_protection
-      - "[0-9]+.[0-9]+.[0-9]+"
-      - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
+  workflow_dispatch:
+    inputs:
+      version:
+        description: 'Release version'
+        required: true
+        default: '1.2.3'
 
 
 # Set permissions at the job level.
 permissions: {}
 
 jobs:
-
-  deploy:
-    if: github.repository == 'pytest-dev/pytest'
-
+  package:
     runs-on: ubuntu-latest
-    timeout-minutes: 30
-    permissions:
-      contents: write
+    env:
+      SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
+    timeout-minutes: 10
 
     steps:
     - uses: actions/checkout@v3
@@ -29,7 +26,19 @@ jobs:
         persist-credentials: false
 
     - name: Build and Check Package
-      uses: hynek/build-and-inspect-python-package@v1.5
+      uses: hynek/build-and-inspect-python-package@v1.5.3
+
+  deploy:
+    if: github.repository == 'pytest-dev/pytest'
+    needs: [package]
+    runs-on: ubuntu-latest
+    environment: deploy
+    timeout-minutes: 30
+    permissions:
+      id-token: write
+      contents: write
+    steps:
+    - uses: actions/checkout@v3
 
     - name: Download Package
       uses: actions/download-artifact@v3
@@ -38,14 +47,35 @@ jobs:
         path: dist
 
     - name: Publish package to PyPI
-      uses: pypa/gh-action-pypi-publish@release/v1
+      uses: pypa/gh-action-pypi-publish@v1.8.5
+
+    - name: Push tag
+      run: |
+        git config user.name "pytest bot"
+        git config user.email "pytestbot@gmail.com"
+        git tag --annotate --message=v${{ github.event.inputs.version }} ${{ github.event.inputs.version }} ${{ github.sha }}
+        git push origin ${{ github.event.inputs.version }}
+
+  release-notes:
+
+    # todo: generate the content in the build  job
+    #       the goal being of using a github action script to push the release data
+    #       after success instead of creating a complete python/tox env
+    needs: [deploy]
+    runs-on: ubuntu-latest
+    timeout-minutes: 30
+    permissions:
+      contents: write
+    steps:
+    - uses: actions/checkout@v3
       with:
-        password: ${{ secrets.pypi_token }}
+        fetch-depth: 0
+        persist-credentials: false
 
     - name: Set up Python
       uses: actions/setup-python@v4
       with:
-        python-version: "3.7"
+        python-version: "3.10"
 
     - name: Install tox
       run: |
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index cf5027223e1..de0bfea5234 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -27,7 +27,19 @@ concurrency:
 permissions: {}
 
 jobs:
+  package:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+      with:
+        fetch-depth: 0
+        persist-credentials: false
+    - name: Build and Check Package
+      uses: hynek/build-and-inspect-python-package@v1.5.3
+
   build:
+    needs: [package]
+
     runs-on: ${{ matrix.os }}
     timeout-minutes: 45
     permissions:
@@ -38,17 +50,17 @@ jobs:
       matrix:
         name: [
           "windows-py37",
-          "windows-py37-pluggy",
           "windows-py38",
+          "windows-py38-pluggy",
           "windows-py39",
           "windows-py310",
           "windows-py311",
           "windows-py312",
 
           "ubuntu-py37",
-          "ubuntu-py37-pluggy",
           "ubuntu-py37-freeze",
           "ubuntu-py38",
+          "ubuntu-py38-pluggy",
           "ubuntu-py39",
           "ubuntu-py310",
           "ubuntu-py311",
@@ -60,7 +72,6 @@ jobs:
           "macos-py310",
           "macos-py312",
 
-          "docs",
           "doctesting",
           "plugins",
         ]
@@ -70,15 +81,15 @@ jobs:
             python: "3.7"
             os: windows-latest
             tox_env: "py37-numpy"
-          - name: "windows-py37-pluggy"
-            python: "3.7"
-            os: windows-latest
-            tox_env: "py37-pluggymain-pylib-xdist"
           - name: "windows-py38"
             python: "3.8"
             os: windows-latest
             tox_env: "py38-unittestextras"
             use_coverage: true
+          - name: "windows-py38-pluggy"
+            python: "3.8"
+            os: windows-latest
+            tox_env: "py38-pluggymain-pylib-xdist"
           - name: "windows-py39"
             python: "3.9"
             os: windows-latest
@@ -101,10 +112,6 @@ jobs:
             os: ubuntu-latest
             tox_env: "py37-lsof-numpy-pexpect"
             use_coverage: true
-          - name: "ubuntu-py37-pluggy"
-            python: "3.7"
-            os: ubuntu-latest
-            tox_env: "py37-pluggymain-pylib-xdist"
           - name: "ubuntu-py37-freeze"
             python: "3.7"
             os: ubuntu-latest
@@ -113,6 +120,10 @@ jobs:
             python: "3.8"
             os: ubuntu-latest
             tox_env: "py38-xdist"
+          - name: "ubuntu-py38-pluggy"
+            python: "3.8"
+            os: ubuntu-latest
+            tox_env: "py38-pluggymain-pylib-xdist"
           - name: "ubuntu-py39"
             python: "3.9"
             os: ubuntu-latest
@@ -159,10 +170,6 @@ jobs:
             os: ubuntu-latest
             tox_env: "plugins"
 
-          - name: "docs"
-            python: "3.7"
-            os: ubuntu-latest
-            tox_env: "docs"
           - name: "doctesting"
             python: "3.7"
             os: ubuntu-latest
@@ -175,6 +182,12 @@ jobs:
         fetch-depth: 0
         persist-credentials: false
 
+    - name: Download Package
+      uses: actions/download-artifact@v3
+      with:
+        name: Packages
+        path: dist
+
     - name: Set up Python ${{ matrix.python }}
       uses: actions/setup-python@v4
       with:
@@ -188,11 +201,13 @@ jobs:
 
     - name: Test without coverage
       if: "! matrix.use_coverage"
-      run: "tox -e ${{ matrix.tox_env }}"
+      shell: bash
+      run: tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz`
 
     - name: Test with coverage
       if: "matrix.use_coverage"
-      run: "tox -e ${{ matrix.tox_env }}-coverage"
+      shell: bash
+      run: tox run -e ${{ matrix.tox_env }}-coverage --installpkg `find dist/*.tar.gz`
 
     - name: Generate coverage report
       if: "matrix.use_coverage"
@@ -206,10 +221,3 @@ jobs:
         fail_ci_if_error: true
         files: ./coverage.xml
         verbose: true
-
-  check-package:
-    runs-on: ubuntu-latest
-    steps:
-    - uses: actions/checkout@v3
-    - name: Build and Check Package
-      uses: hynek/build-and-inspect-python-package@v1.5
diff --git a/.readthedocs.yml b/.readthedocs.yml
index b506c5f4039..266d4e07aea 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -9,6 +9,10 @@ python:
        path: .
      - requirements: doc/en/requirements.txt
 
+sphinx:
+  configuration: doc/en/conf.py
+  fail_on_warning: true
+
 build:
   os: ubuntu-20.04
   tools:
diff --git a/AUTHORS b/AUTHORS
index ee4ef203f0c..3c4009603a2 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -47,6 +47,7 @@ Ariel Pillemer
 Armin Rigo
 Aron Coyle
 Aron Curzon
+Arthur Richard
 Ashish Kurmi
 Aviral Verma
 Aviv Palivoda
@@ -166,6 +167,8 @@ Ian Bicking
 Ian Lesperance
 Ilya Konstantinov
 Ionuț Turturică
+Isaac Virshup
+Israel Fruchter
 Itxaso Aizpurua
 Iwan Briquemont
 Jaap Broekhuizen
@@ -173,6 +176,7 @@ Jake VanderPlas
 Jakob van Santen
 Jakub Mitoraj
 James Bourbeau
+James Frost
 Jan Balster
 Janne Vanhala
 Jason R. Coombs
@@ -229,6 +233,7 @@ Maho
 Maik Figura
 Mandeep Bhutani
 Manuel Krebber
+Marc Mueller
 Marc Schlaich
 Marcelo Duarte Trevisani
 Marcin Bachry
@@ -320,6 +325,7 @@ Ronny Pfannschmidt
 Ross Lawley
 Ruaridh Williamson
 Russel Winder
+Ryan Puddephatt
 Ryan Wooden
 Saiprasad Kale
 Samuel Colvin
@@ -334,11 +340,13 @@ Serhii Mozghovyi
 Seth Junot
 Shantanu Jain
 Shubham Adep
+Simon Blanchard
 Simon Gomizelj
 Simon Holesch
 Simon Kerr
 Skylar Downes
 Srinivas Reddy Thatiparthy
+Stefaan Lippens
 Stefan Farmbauer
 Stefan Scherfke
 Stefan Zimmermann
@@ -370,7 +378,9 @@ Tomer Keren
 Tony Narlock
 Tor Colvin
 Trevor Bekolay
+Tushar Sadhwani
 Tyler Goodlet
+Tyler Smart
 Tzu-ping Chung
 Vasily Kuznetsov
 Victor Maryama
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 791f988306f..5ccc66bf5c4 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -50,7 +50,7 @@ Fix bugs
 --------
 
 Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_.
-See also the `"status: easy" issues <https://github.com/pytest-dev/pytest/labels/status%3A%20easy>`_
+See also the `"good first issue" issues <https://github.com/pytest-dev/pytest/labels/good%20first%20issue>`_
 that are friendly to new contributors.
 
 :ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going
diff --git a/RELEASING.rst b/RELEASING.rst
index b018dc48932..08004a84c00 100644
--- a/RELEASING.rst
+++ b/RELEASING.rst
@@ -133,14 +133,12 @@ Releasing
 
 Both automatic and manual processes described above follow the same steps from this point onward.
 
-#. After all tests pass and the PR has been approved, tag the release commit
-   in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI::
+#. After all tests pass and the PR has been approved, trigger the ``deploy`` job
+   in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml, using the ``release-MAJOR.MINOR.PATCH`` branch
+   as source.
 
-     git fetch upstream
-     git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
-     git push upstream MAJOR.MINOR.PATCH
-
-   Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
+   This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI
+   and tag the repository.
 
 #. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.
 
diff --git a/changelog/10831.bugfix.rst b/changelog/10831.bugfix.rst
deleted file mode 100644
index ea641dee60e..00000000000
--- a/changelog/10831.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.
diff --git a/changelog/10872.improvement.rst b/changelog/10872.improvement.rst
deleted file mode 100644
index fe0c01a0224..00000000000
--- a/changelog/10872.improvement.rst
+++ /dev/null
@@ -1 +0,0 @@
-Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.
diff --git a/changelog/10901.feature.rst b/changelog/10901.feature.rst
deleted file mode 100644
index 0d99d66f67f..00000000000
--- a/changelog/10901.feature.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
-This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.
diff --git a/changelog/10907.improvement.rst b/changelog/10907.improvement.rst
deleted file mode 100644
index 7f011a827d3..00000000000
--- a/changelog/10907.improvement.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
-
-"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
-
-Previously, the last frame of the traceback was shown, even though it was hidden.
diff --git a/changelog/10940.improvement.rst b/changelog/10940.improvement.rst
deleted file mode 100644
index f3e33cbc68d..00000000000
--- a/changelog/10940.improvement.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
-
-Added :func:`TerminalReporter.wrap_write() <pytest.TerminalReporter.wrap_write>` as a helper for that.
diff --git a/changelog/10991.improvement.rst b/changelog/10991.improvement.rst
deleted file mode 100644
index 768c08e554c..00000000000
--- a/changelog/10991.improvement.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.
diff --git a/changelog/11005.improvement.rst b/changelog/11005.improvement.rst
deleted file mode 100644
index 295252514a6..00000000000
--- a/changelog/11005.improvement.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added underlying exception to cache provider path creation and write warning messages.
diff --git a/changelog/11013.improvement.rst b/changelog/11013.improvement.rst
deleted file mode 100644
index fe3ece93c23..00000000000
--- a/changelog/11013.improvement.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory.
diff --git a/changelog/11031.trivial.rst b/changelog/11031.trivial.rst
deleted file mode 100644
index fad2cf24d5a..00000000000
--- a/changelog/11031.trivial.rst
+++ /dev/null
@@ -1 +0,0 @@
-Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file.
diff --git a/changelog/11043.improvement.rst b/changelog/11043.improvement.rst
deleted file mode 100644
index 1fe0361d7f9..00000000000
--- a/changelog/11043.improvement.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir`.
-Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem.
-If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`.
diff --git a/changelog/11068.bugfix.rst b/changelog/11068.bugfix.rst
deleted file mode 100644
index 45cdb105fb4..00000000000
--- a/changelog/11068.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`.
diff --git a/changelog/11081.improvement.rst b/changelog/11081.improvement.rst
deleted file mode 100644
index ccfaf6016ae..00000000000
--- a/changelog/11081.improvement.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-The :confval:`norecursedir` check is now performed in a :hook:`pytest_ignore_collect` implementation, so plugins can affect it.
-
-If after updating to this version you see that your `norecursedir` setting is not being respected,
-it means that a conftest or a plugin you use has a bad `pytest_ignore_collect` implementation.
-Most likely, your hook returns `False` for paths it does not want to ignore,
-which ends the processing and doesn't allow other plugins, including pytest itself, to ignore the path.
-The fix is to return `None` instead of `False` for paths your hook doesn't want to ignore.
diff --git a/changelog/11104.bugfix.rst b/changelog/11104.bugfix.rst
deleted file mode 100644
index 10f0db92515..00000000000
--- a/changelog/11104.bugfix.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-Fixed a regression in pytest 7.3.2 which caused to :confval:`testpaths` to be considered for loading initial conftests,
-even when it was not utilized (e.g. when explicit paths were given on the command line).
-Now the ``testpaths`` are only considered when they are in use.
diff --git a/changelog/12363.doc.rst b/changelog/12363.doc.rst
new file mode 100644
index 00000000000..c657281babf
--- /dev/null
+++ b/changelog/12363.doc.rst
@@ -0,0 +1 @@
+The documentation webpages now links to a canonical version to reduce outdated documentation in search engine results.
diff --git a/changelog/1904.bugfix.rst b/changelog/1904.bugfix.rst
deleted file mode 100644
index 3e1a292152b..00000000000
--- a/changelog/1904.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).
diff --git a/changelog/7781.bugfix.rst b/changelog/7781.bugfix.rst
deleted file mode 100644
index 191fcd4daf4..00000000000
--- a/changelog/7781.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fix writing non-encodable text to log file when using ``--debug``.
diff --git a/changelog/8711.improvement.rst b/changelog/8711.improvement.rst
deleted file mode 100644
index 20805e9f94d..00000000000
--- a/changelog/8711.improvement.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-:func:`_pytest.logging.LogCaptureFixture.set_level` and :func:`_pytest.logging.LogCaptureFixture.at_level`
-will temporarily enable the requested ``level`` if ``level`` was disabled globally via
-``logging.disable(LEVEL)``.
diff --git a/changelog/9146.doc.rst b/changelog/9146.doc.rst
deleted file mode 100644
index 95189b96d46..00000000000
--- a/changelog/9146.doc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Improve Documentation for `caplog.set_level`.
diff --git a/changelog/README.rst b/changelog/README.rst
index 6d026f57ef3..88956ef28d8 100644
--- a/changelog/README.rst
+++ b/changelog/README.rst
@@ -14,7 +14,7 @@ Each file should be named like ``<ISSUE>.<TYPE>.rst``, where
 ``<ISSUE>`` is an issue number, and ``<TYPE>`` is one of:
 
 * ``feature``: new user facing features, like new command-line options and new behavior.
-* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junitxml``, improved colors in terminal, etc).
+* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junit-xml``, improved colors in terminal, etc).
 * ``bugfix``: fixes a bug.
 * ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
 * ``deprecation``: feature deprecation.
diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst
index bcc0669a62f..35fd2c814e2 100644
--- a/doc/en/announce/index.rst
+++ b/doc/en/announce/index.rst
@@ -6,6 +6,11 @@ Release announcements
    :maxdepth: 2
 
 
+   release-7.4.4
+   release-7.4.3
+   release-7.4.2
+   release-7.4.1
+   release-7.4.0
    release-7.3.2
    release-7.3.1
    release-7.3.0
diff --git a/doc/en/announce/release-7.4.0.rst b/doc/en/announce/release-7.4.0.rst
new file mode 100644
index 00000000000..5a0d18267d3
--- /dev/null
+++ b/doc/en/announce/release-7.4.0.rst
@@ -0,0 +1,49 @@
+pytest-7.4.0
+=======================================
+
+The pytest team is proud to announce the 7.4.0 release!
+
+This release contains new features, improvements, and bug fixes,
+the full list of changes is available in the changelog:
+
+    https://docs.pytest.org/en/stable/changelog.html
+
+For complete documentation, please visit:
+
+    https://docs.pytest.org/en/stable/
+
+As usual, you can upgrade from PyPI via:
+
+    pip install -U pytest
+
+Thanks to all of the contributors to this release:
+
+* Adam J. Stewart
+* Alessio Izzo
+* Alex
+* Alex Lambson
+* Brian Larsen
+* Bruno Oliveira
+* Bryan Ricker
+* Chris Mahoney
+* Facundo Batista
+* Florian Bruhin
+* Jarrett Keifer
+* Kenny Y
+* Miro Hrončok
+* Ran Benita
+* Roberto Aldera
+* Ronny Pfannschmidt
+* Sergey Kim
+* Stefanie Molin
+* Vijay Arora
+* Ville Skyttä
+* Zac Hatfield-Dodds
+* bzoracler
+* leeyueh
+* nondescryptid
+* theirix
+
+
+Happy testing,
+The pytest Development Team
diff --git a/doc/en/announce/release-7.4.1.rst b/doc/en/announce/release-7.4.1.rst
new file mode 100644
index 00000000000..efadcf919e8
--- /dev/null
+++ b/doc/en/announce/release-7.4.1.rst
@@ -0,0 +1,20 @@
+pytest-7.4.1
+=======================================
+
+pytest 7.4.1 has just been released to PyPI.
+
+This is a bug-fix release, being a drop-in replacement. To upgrade::
+
+  pip install --upgrade pytest
+
+The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
+
+Thanks to all of the contributors to this release:
+
+* Bruno Oliveira
+* Florian Bruhin
+* Ran Benita
+
+
+Happy testing,
+The pytest Development Team
diff --git a/doc/en/announce/release-7.4.2.rst b/doc/en/announce/release-7.4.2.rst
new file mode 100644
index 00000000000..22191e7b4f9
--- /dev/null
+++ b/doc/en/announce/release-7.4.2.rst
@@ -0,0 +1,18 @@
+pytest-7.4.2
+=======================================
+
+pytest 7.4.2 has just been released to PyPI.
+
+This is a bug-fix release, being a drop-in replacement. To upgrade::
+
+  pip install --upgrade pytest
+
+The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
+
+Thanks to all of the contributors to this release:
+
+* Bruno Oliveira
+
+
+Happy testing,
+The pytest Development Team
diff --git a/doc/en/announce/release-7.4.3.rst b/doc/en/announce/release-7.4.3.rst
new file mode 100644
index 00000000000..0f319c1e7f0
--- /dev/null
+++ b/doc/en/announce/release-7.4.3.rst
@@ -0,0 +1,19 @@
+pytest-7.4.3
+=======================================
+
+pytest 7.4.3 has just been released to PyPI.
+
+This is a bug-fix release, being a drop-in replacement. To upgrade::
+
+  pip install --upgrade pytest
+
+The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
+
+Thanks to all of the contributors to this release:
+
+* Bruno Oliveira
+* Marc Mueller
+
+
+Happy testing,
+The pytest Development Team
diff --git a/doc/en/announce/release-7.4.4.rst b/doc/en/announce/release-7.4.4.rst
new file mode 100644
index 00000000000..c9633678d2e
--- /dev/null
+++ b/doc/en/announce/release-7.4.4.rst
@@ -0,0 +1,20 @@
+pytest-7.4.4
+=======================================
+
+pytest 7.4.4 has just been released to PyPI.
+
+This is a bug-fix release, being a drop-in replacement. To upgrade::
+
+  pip install --upgrade pytest
+
+The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
+
+Thanks to all of the contributors to this release:
+
+* Bruno Oliveira
+* Ran Benita
+* Zac Hatfield-Dodds
+
+
+Happy testing,
+The pytest Development Team
diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst
index 7ae185f6c7d..405289444a8 100644
--- a/doc/en/builtin.rst
+++ b/doc/en/builtin.rst
@@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
     cachedir: .pytest_cache
     rootdir: /home/sweet/project
     collected 0 items
-    cache -- .../_pytest/cacheprovider.py:510
+    cache -- .../_pytest/cacheprovider.py:532
         Return a cache object that can persist state between testing sessions.
 
         cache.get(key, default)
@@ -105,7 +105,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
                 captured = capsys.readouterr()
                 assert captured.out == "hello\n"
 
-    doctest_namespace [session scope] -- .../_pytest/doctest.py:737
+    doctest_namespace [session scope] -- .../_pytest/doctest.py:757
         Fixture that returns a :py:class:`dict` that will be injected into the
         namespace of doctests.
 
@@ -119,7 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
 
         For more details: :ref:`doctest_namespace`.
 
-    pytestconfig [session scope] -- .../_pytest/fixtures.py:1360
+    pytestconfig [session scope] -- .../_pytest/fixtures.py:1353
         Session-scoped fixture that returns the session's :class:`pytest.Config`
         object.
 
@@ -196,7 +196,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
 
         .. _legacy_path: https://py.readthedocs.io/en/latest/path.html
 
-    caplog -- .../_pytest/logging.py:498
+    caplog -- .../_pytest/logging.py:570
         Access and control log capturing.
 
         Captured logs are available through the following properties/methods::
diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst
index cdc9e66f68b..6973a08c9c6 100644
--- a/doc/en/changelog.rst
+++ b/doc/en/changelog.rst
@@ -28,6 +28,180 @@ with advance notice in the **Deprecations** section of releases.
 
 .. towncrier release notes start
 
+pytest 7.4.4 (2023-12-31)
+=========================
+
+Bug Fixes
+---------
+
+- `#11140 <https://github.com/pytest-dev/pytest/issues/11140>`_: Fix non-string constants at the top of file being detected as docstrings on Python>=3.8.
+
+
+- `#11572 <https://github.com/pytest-dev/pytest/issues/11572>`_: Handle an edge case where :data:`sys.stderr` and :data:`sys.__stderr__` might already be closed when :ref:`faulthandler` is tearing down.
+
+
+- `#11710 <https://github.com/pytest-dev/pytest/issues/11710>`_: Fixed tracebacks from collection errors not getting pruned.
+
+
+- `#7966 <https://github.com/pytest-dev/pytest/issues/7966>`_: Removed unhelpful error message from assertion rewrite mechanism when exceptions are raised in ``__iter__`` methods. Now they are treated un-iterable instead.
+
+
+
+Improved Documentation
+----------------------
+
+- `#11091 <https://github.com/pytest-dev/pytest/issues/11091>`_: Updated documentation to refer to hyphenated options: replaced ``--junitxml`` with ``--junit-xml`` and ``--collectonly`` with ``--collect-only``.
+
+
+pytest 7.4.3 (2023-10-24)
+=========================
+
+Bug Fixes
+---------
+
+- `#10447 <https://github.com/pytest-dev/pytest/issues/10447>`_: Markers are now considered in the reverse mro order to ensure base  class markers are considered first -- this resolves a regression.
+
+
+- `#11239 <https://github.com/pytest-dev/pytest/issues/11239>`_: Fixed ``:=`` in asserts impacting unrelated test cases.
+
+
+- `#11439 <https://github.com/pytest-dev/pytest/issues/11439>`_: Handled an edge case where :data:`sys.stderr` might already be closed when :ref:`faulthandler` is tearing down.
+
+
+pytest 7.4.2 (2023-09-07)
+=========================
+
+Bug Fixes
+---------
+
+- `#11237 <https://github.com/pytest-dev/pytest/issues/11237>`_: Fix doctest collection of `functools.cached_property` objects.
+
+
+- `#11306 <https://github.com/pytest-dev/pytest/issues/11306>`_: Fixed bug using ``--importmode=importlib`` which would cause package ``__init__.py`` files to be imported more than once in some cases.
+
+
+- `#11367 <https://github.com/pytest-dev/pytest/issues/11367>`_: Fixed bug where `user_properties` where not being saved in the JUnit XML file if a fixture failed during teardown.
+
+
+- `#11394 <https://github.com/pytest-dev/pytest/issues/11394>`_: Fixed crash when parsing long command line arguments that might be interpreted as files.
+
+
+
+Improved Documentation
+----------------------
+
+- `#11391 <https://github.com/pytest-dev/pytest/issues/11391>`_: Improved disclaimer on pytest plugin reference page to better indicate this is an automated, non-curated listing.
+
+
+pytest 7.4.1 (2023-09-02)
+=========================
+
+Bug Fixes
+---------
+
+- `#10337 <https://github.com/pytest-dev/pytest/issues/10337>`_: Fixed bug where fake intermediate modules generated by ``--import-mode=importlib`` would not include the
+  child modules as attributes of the parent modules.
+
+
+- `#10702 <https://github.com/pytest-dev/pytest/issues/10702>`_: Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries.
+
+
+- `#10811 <https://github.com/pytest-dev/pytest/issues/10811>`_: Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules
+  to be imported more than once, causing problems with modules that have import side effects.
+
+
+pytest 7.4.0 (2023-06-23)
+=========================
+
+Features
+--------
+
+- `#10901 <https://github.com/pytest-dev/pytest/issues/10901>`_: Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
+  This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.
+
+
+
+Improvements
+------------
+
+- `#10872 <https://github.com/pytest-dev/pytest/issues/10872>`_: Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.
+
+
+- `#10907 <https://github.com/pytest-dev/pytest/issues/10907>`_: When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
+
+  "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
+
+  Previously, the last frame of the traceback was shown, even though it was hidden.
+
+
+- `#10940 <https://github.com/pytest-dev/pytest/issues/10940>`_: Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
+
+  Added ``TerminalReporter.wrap_write()`` as a helper for that.
+
+
+- `#10991 <https://github.com/pytest-dev/pytest/issues/10991>`_: Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.
+
+
+- `#11005 <https://github.com/pytest-dev/pytest/issues/11005>`_: Added the underlying exception to the cache provider's path creation and write warning messages.
+
+
+- `#11013 <https://github.com/pytest-dev/pytest/issues/11013>`_: Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory.
+
+
+- `#11043 <https://github.com/pytest-dev/pytest/issues/11043>`_: When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir <rootdir>`.
+  Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem.
+  If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`.
+
+
+- `#11081 <https://github.com/pytest-dev/pytest/issues/11081>`_: The :confval:`norecursedirs` check is now performed in a :hook:`pytest_ignore_collect` implementation, so plugins can affect it.
+
+  If after updating to this version you see that your `norecursedirs` setting is not being respected,
+  it means that a conftest or a plugin you use has a bad `pytest_ignore_collect` implementation.
+  Most likely, your hook returns `False` for paths it does not want to ignore,
+  which ends the processing and doesn't allow other plugins, including pytest itself, to ignore the path.
+  The fix is to return `None` instead of `False` for paths your hook doesn't want to ignore.
+
+
+- `#8711 <https://github.com/pytest-dev/pytest/issues/8711>`_: :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` and :func:`caplog.at_level() <pytest.LogCaptureFixture.at_level>`
+  will temporarily enable the requested ``level`` if ``level`` was disabled globally via
+  ``logging.disable(LEVEL)``.
+
+
+
+Bug Fixes
+---------
+
+- `#10831 <https://github.com/pytest-dev/pytest/issues/10831>`_: Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.
+
+
+- `#11068 <https://github.com/pytest-dev/pytest/issues/11068>`_: Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`.
+
+
+- `#11104 <https://github.com/pytest-dev/pytest/issues/11104>`_: Fixed a regression in pytest 7.3.2 which caused to :confval:`testpaths` to be considered for loading initial conftests,
+  even when it was not utilized (e.g. when explicit paths were given on the command line).
+  Now the ``testpaths`` are only considered when they are in use.
+
+
+- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).
+
+
+- `#7781 <https://github.com/pytest-dev/pytest/issues/7781>`_: Fix writing non-encodable text to log file when using ``--debug``.
+
+
+
+Improved Documentation
+----------------------
+
+- `#9146 <https://github.com/pytest-dev/pytest/issues/9146>`_: Improved documentation for :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>`.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- `#11031 <https://github.com/pytest-dev/pytest/issues/11031>`_: Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file.
+
+
 pytest 7.3.2 (2023-06-10)
 =========================
 
diff --git a/doc/en/conf.py b/doc/en/conf.py
index 32f508219a6..27dbfe1ebfd 100644
--- a/doc/en/conf.py
+++ b/doc/en/conf.py
@@ -273,6 +273,9 @@
 # Output file base name for HTML help builder.
 htmlhelp_basename = "pytestdoc"
 
+# The base URL which points to the root of the HTML documentation. It is used
+# to indicate the location of document using the canonical link relation (#12363).
+html_baseurl = "https://docs.pytest.org/en/stable/"
 
 # -- Options for LaTeX output --------------------------------------------------
 
diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst
index 4f7830a2791..3bb93d81ef6 100644
--- a/doc/en/deprecations.rst
+++ b/doc/en/deprecations.rst
@@ -596,7 +596,7 @@ By using ``legacy`` you will keep using the legacy/xunit1 format when upgrading
 pytest 6.0, where the default format will be ``xunit2``.
 
 In order to let users know about the transition, pytest will issue a warning in case
-the ``--junitxml`` option is given in the command line but ``junit_family`` is not explicitly
+the ``--junit-xml`` option is given in the command line but ``junit_family`` is not explicitly
 configured in ``pytest.ini``.
 
 Services known to support the ``xunit2`` format:
diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst
index 55fd1f576cf..6cdf4eb42d8 100644
--- a/doc/en/example/markers.rst
+++ b/doc/en/example/markers.rst
@@ -136,7 +136,7 @@ Or select multiple nodes:
 
     Node IDs for failing tests are displayed in the test summary info
     when running pytest with the ``-rf`` option.  You can also
-    construct Node IDs from the output of ``pytest --collectonly``.
+    construct Node IDs from the output of ``pytest --collect-only``.
 
 Using ``-k expr`` to select tests based on their name
 -------------------------------------------------------
diff --git a/doc/en/example/nonpython/conftest.py b/doc/en/example/nonpython/conftest.py
index dd1ebe88d7e..e969e3e2518 100644
--- a/doc/en/example/nonpython/conftest.py
+++ b/doc/en/example/nonpython/conftest.py
@@ -12,7 +12,7 @@ def collect(self):
         # We need a yaml parser, e.g. PyYAML.
         import yaml
 
-        raw = yaml.safe_load(self.path.open())
+        raw = yaml.safe_load(self.path.open(encoding="utf-8"))
         for name, spec in sorted(raw.items()):
             yield YamlItem.from_parent(self, name=name, spec=spec)
 
diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst
index 97a6dd9f436..ba2920daf6f 100644
--- a/doc/en/example/simple.rst
+++ b/doc/en/example/simple.rst
@@ -817,7 +817,7 @@ case we just write some information out to a ``failures`` file:
         # we only look at actual failing test calls, not setup/teardown
         if rep.when == "call" and rep.failed:
             mode = "a" if os.path.exists("failures") else "w"
-            with open("failures", mode) as f:
+            with open("failures", mode, encoding="utf-8") as f:
                 # let's also access a fixture for the fun of it
                 if "tmp_path" in item.fixturenames:
                     extra = " ({})".format(item.funcargs["tmp_path"])
@@ -1088,4 +1088,4 @@ application with standard ``pytest`` command-line options:
 
 .. code-block:: bash
 
-    ./app_main --pytest --verbose --tb=long --junitxml=results.xml test-suite/
+    ./app_main --pytest --verbose --tb=long --junit=xml=results.xml test-suite/
diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst
index 2dbf7d3870c..cd9ef9a66d3 100644
--- a/doc/en/getting-started.rst
+++ b/doc/en/getting-started.rst
@@ -22,7 +22,7 @@ Install ``pytest``
 .. code-block:: bash
 
     $ pytest --version
-    pytest 7.3.2
+    pytest 7.4.4
 
 .. _`simpletest`:
 
diff --git a/doc/en/how-to/assert.rst b/doc/en/how-to/assert.rst
index 1b10c131389..d99a1ce5c97 100644
--- a/doc/en/how-to/assert.rst
+++ b/doc/en/how-to/assert.rst
@@ -54,14 +54,13 @@ operators. (See :ref:`tbreportdemo`).  This allows you to use the
 idiomatic python constructs without boilerplate code while not losing
 introspection information.
 
-However, if you specify a message with the assertion like this:
+If a message is specified with the assertion like this:
 
 .. code-block:: python
 
     assert a % 2 == 0, "value was odd, should be even"
 
-then no assertion introspection takes places at all and the message
-will be simply shown in the traceback.
+it is printed alongside the assertion introspection in the traceback.
 
 See :ref:`assert-details` for more information on assertion introspection.
 
diff --git a/doc/en/how-to/cache.rst b/doc/en/how-to/cache.rst
index 8554a984c72..03ab0c77780 100644
--- a/doc/en/how-to/cache.rst
+++ b/doc/en/how-to/cache.rst
@@ -176,14 +176,21 @@ with more recent files coming first.
 Behavior when no tests failed in the last run
 ---------------------------------------------
 
-When no tests failed in the last run, or when no cached ``lastfailed`` data was
-found, ``pytest`` can be configured either to run all of the tests or no tests,
-using the ``--last-failed-no-failures`` option, which takes one of the following values:
+The ``--lfnf/--last-failed-no-failures`` option governs the behavior of ``--last-failed``.
+Determines whether to execute tests when there are no previously (known)
+failures or when no cached ``lastfailed`` data was found.
+
+There are two options:
+
+* ``all``:  when there are no known test failures, runs all tests (the full test suite). This is the default.
+* ``none``: when there are no known test failures, just emits a message stating this and exit successfully.
+
+Example:
 
 .. code-block:: bash
 
-    pytest --last-failed --last-failed-no-failures all    # run all tests (default behavior)
-    pytest --last-failed --last-failed-no-failures none   # run no tests and exit
+    pytest --last-failed --last-failed-no-failures all    # runs the full test suite (default behavior)
+    pytest --last-failed --last-failed-no-failures none   # runs no tests and exits successfully
 
 The new config.cache object
 --------------------------------
diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst
index d8517c2c8f3..a6f4962f083 100644
--- a/doc/en/how-to/fixtures.rst
+++ b/doc/en/how-to/fixtures.rst
@@ -1698,7 +1698,7 @@ and declare its use in a test module via a ``usefixtures`` marker:
     class TestDirectoryInit:
         def test_cwd_starts_empty(self):
             assert os.listdir(os.getcwd()) == []
-            with open("myfile", "w") as f:
+            with open("myfile", "w", encoding="utf-8") as f:
                 f.write("hello")
 
         def test_cwd_again_starts_empty(self):
diff --git a/doc/en/how-to/output.rst b/doc/en/how-to/output.rst
index e8e9af0c70b..cf72a2d291c 100644
--- a/doc/en/how-to/output.rst
+++ b/doc/en/how-to/output.rst
@@ -478,7 +478,7 @@ integration servers, use this invocation:
 
 .. code-block:: bash
 
-    pytest --junitxml=path
+    pytest --junit-xml=path
 
 to create an XML file at ``path``.
 
diff --git a/doc/en/how-to/tmp_path.rst b/doc/en/how-to/tmp_path.rst
index 792933dd87e..3b49d63a5be 100644
--- a/doc/en/how-to/tmp_path.rst
+++ b/doc/en/how-to/tmp_path.rst
@@ -24,8 +24,8 @@ created in the `base temporary directory`_.
         d = tmp_path / "sub"
         d.mkdir()
         p = d / "hello.txt"
-        p.write_text(CONTENT)
-        assert p.read_text() == CONTENT
+        p.write_text(CONTENT, encoding="utf-8")
+        assert p.read_text(encoding="utf-8") == CONTENT
         assert len(list(tmp_path.iterdir())) == 1
         assert 0
 
@@ -51,8 +51,8 @@ Running this would result in a passed test except for the last
             d = tmp_path / "sub"
             d.mkdir()
             p = d / "hello.txt"
-            p.write_text(CONTENT)
-            assert p.read_text() == CONTENT
+            p.write_text(CONTENT, encoding="utf-8")
+            assert p.read_text(encoding="utf-8") == CONTENT
             assert len(list(tmp_path.iterdir())) == 1
     >       assert 0
     E       assert 0
diff --git a/doc/en/how-to/unittest.rst b/doc/en/how-to/unittest.rst
index 37caf6e9fb7..7856c1a49c0 100644
--- a/doc/en/how-to/unittest.rst
+++ b/doc/en/how-to/unittest.rst
@@ -207,10 +207,10 @@ creation of a per-test temporary directory:
         @pytest.fixture(autouse=True)
         def initdir(self, tmp_path, monkeypatch):
             monkeypatch.chdir(tmp_path)  # change to pytest-provided temporary directory
-            tmp_path.joinpath("samplefile.ini").write_text("# testdata")
+            tmp_path.joinpath("samplefile.ini").write_text("# testdata", encoding="utf-8")
 
         def test_method(self):
-            with open("samplefile.ini") as f:
+            with open("samplefile.ini", encoding="utf-8") as f:
                 s = f.read()
             assert "testdata" in s
 
diff --git a/doc/en/how-to/usage.rst b/doc/en/how-to/usage.rst
index 8e2dd86736f..4ec4459a232 100644
--- a/doc/en/how-to/usage.rst
+++ b/doc/en/how-to/usage.rst
@@ -173,7 +173,8 @@ You can invoke ``pytest`` from Python code directly:
 
 this acts as if you would call "pytest" from the command line.
 It will not raise :class:`SystemExit` but return the :ref:`exit code <exit-codes>` instead.
-You can pass in options and arguments:
+If you don't pass it any arguments, ``main`` reads the arguments from the command line arguments of the process (:data:`sys.argv`), which may be undesirable.
+You can pass in options and arguments explicitly:
 
 .. code-block:: python
 
diff --git a/doc/en/index.rst b/doc/en/index.rst
index de07831ac14..046c012f95b 100644
--- a/doc/en/index.rst
+++ b/doc/en/index.rst
@@ -1,9 +1,11 @@
 :orphan:
 
-.. sidebar:: Next Open Trainings
+.. sidebar:: Next Open Trainings and Events
 
-   - `pytest tips and tricks for a better testsuite <https://ep2023.europython.eu/session/pytest-tips-and-tricks-for-a-better-testsuite>`_, at `Europython 2023 <https://ep2023.europython.eu/>`_, July 18th (3h), Prague/Remote
-   - `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 5th to 7th 2024 (3 day in-depth training), Leipzig/Remote
+   - `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_ (3 day in-depth training):
+      * **June 11th to 13th 2024**, Remote
+      * **March 4th to 6th 2025**, Leipzig, Germany / Remote
+   - `pytest development sprint <https://github.com/pytest-dev/pytest/discussions/11655>`_, June 2024 (`date poll <https://nuudel.digitalcourage.de/2tEsEpRcwMNcAXVO>`_)
 
    Also see :doc:`previous talks and blogposts <talks>`.
 
diff --git a/doc/en/reference/customize.rst b/doc/en/reference/customize.rst
index b794d646b8e..24c0ed21752 100644
--- a/doc/en/reference/customize.rst
+++ b/doc/en/reference/customize.rst
@@ -90,7 +90,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se
 setup.cfg
 ~~~~~~~~~
 
-``setup.cfg`` files are general purpose configuration files, used originally by :doc:`distutils <python:distutils/configfile>`, and can also be used to hold pytest configuration
+``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and `setuptools <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`__, and can also be used to hold pytest configuration
 if they have a ``[tool:pytest]`` section.
 
 .. code-block:: ini
diff --git a/doc/en/reference/plugin_list.rst b/doc/en/reference/plugin_list.rst
index c882130b03e..42b6a9a8f29 100644
--- a/doc/en/reference/plugin_list.rst
+++ b/doc/en/reference/plugin_list.rst
@@ -1,14 +1,26 @@
 
 .. _plugin-list:
 
-Plugin List
-===========
+Pytest Plugin List
+==================
 
-PyPI projects that match "pytest-\*" are considered plugins and are listed
-automatically together with a manually-maintained list in `the source
-code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
+Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
+It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
 Packages classified as inactive are excluded.
 
+For detailed insights into how this list is generated,
+please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
+
+.. warning::
+
+   Please be aware that this list is not a curated collection of projects
+   and does not undergo a systematic review process.
+   It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins.
+
+   Do not presume any endorsement from the ``pytest`` project or its developers,
+   and always conduct your own quality assessment before incorporating any of these plugins into your own projects.
+
+
 .. The following conditional uses a different format for this list when
    creating a PDF, because otherwise the table gets far too wide for the
    page.
diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst
index c6629db84fa..c80bdb18eb5 100644
--- a/doc/en/reference/reference.rst
+++ b/doc/en/reference/reference.rst
@@ -1,3 +1,5 @@
+:tocdepth: 3
+
 .. _`api-reference`:
 
 API Reference
@@ -77,11 +79,13 @@ pytest.xfail
 pytest.exit
 ~~~~~~~~~~~
 
-.. autofunction:: pytest.exit(reason, [returncode=False, msg=None])
+.. autofunction:: pytest.exit(reason, [returncode=None, msg=None])
 
 pytest.main
 ~~~~~~~~~~~
 
+**Tutorial**: :ref:`pytest.main-usage`
+
 .. autofunction:: pytest.main
 
 pytest.param
@@ -783,25 +787,17 @@ reporting or interaction with exceptions:
 .. autofunction:: pytest_leave_pdb
 
 
-Objects
--------
-
-Full reference to objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`.
+Collection tree objects
+-----------------------
 
+These are the collector and item classes (collectively called "nodes") which
+make up the collection tree.
 
-CallInfo
-~~~~~~~~
-
-.. autoclass:: pytest.CallInfo()
-    :members:
-
-
-Class
-~~~~~
+Node
+~~~~
 
-.. autoclass:: pytest.Class()
+.. autoclass:: _pytest.nodes.Node()
     :members:
-    :show-inheritance:
 
 Collector
 ~~~~~~~~~
@@ -810,52 +806,52 @@ Collector
     :members:
     :show-inheritance:
 
-CollectReport
-~~~~~~~~~~~~~
+Item
+~~~~
 
-.. autoclass:: pytest.CollectReport()
+.. autoclass:: pytest.Item()
     :members:
     :show-inheritance:
-    :inherited-members:
 
-Config
-~~~~~~
+File
+~~~~
 
-.. autoclass:: pytest.Config()
+.. autoclass:: pytest.File()
     :members:
+    :show-inheritance:
 
-ExceptionInfo
-~~~~~~~~~~~~~
+FSCollector
+~~~~~~~~~~~
 
-.. autoclass:: pytest.ExceptionInfo()
+.. autoclass:: _pytest.nodes.FSCollector()
     :members:
+    :show-inheritance:
 
+Session
+~~~~~~~
 
-ExitCode
-~~~~~~~~
-
-.. autoclass:: pytest.ExitCode
+.. autoclass:: pytest.Session()
     :members:
+    :show-inheritance:
 
-File
-~~~~
+Package
+~~~~~~~
 
-.. autoclass:: pytest.File()
+.. autoclass:: pytest.Package()
     :members:
     :show-inheritance:
 
+Module
+~~~~~~
 
-FixtureDef
-~~~~~~~~~~
-
-.. autoclass:: _pytest.fixtures.FixtureDef()
+.. autoclass:: pytest.Module()
     :members:
     :show-inheritance:
 
-FSCollector
-~~~~~~~~~~~
+Class
+~~~~~
 
-.. autoclass:: _pytest.nodes.FSCollector()
+.. autoclass:: pytest.Class()
     :members:
     :show-inheritance:
 
@@ -873,10 +869,52 @@ FunctionDefinition
     :members:
     :show-inheritance:
 
-Item
-~~~~
 
-.. autoclass:: pytest.Item()
+Objects
+-------
+
+Objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`
+or importable from ``pytest``.
+
+
+CallInfo
+~~~~~~~~
+
+.. autoclass:: pytest.CallInfo()
+    :members:
+
+CollectReport
+~~~~~~~~~~~~~
+
+.. autoclass:: pytest.CollectReport()
+    :members:
+    :show-inheritance:
+    :inherited-members:
+
+Config
+~~~~~~
+
+.. autoclass:: pytest.Config()
+    :members:
+
+ExceptionInfo
+~~~~~~~~~~~~~
+
+.. autoclass:: pytest.ExceptionInfo()
+    :members:
+
+
+ExitCode
+~~~~~~~~
+
+.. autoclass:: pytest.ExitCode
+    :members:
+
+
+FixtureDef
+~~~~~~~~~~
+
+.. autoclass:: _pytest.fixtures.FixtureDef()
     :members:
     :show-inheritance:
 
@@ -907,19 +945,6 @@ Metafunc
 .. autoclass:: pytest.Metafunc()
     :members:
 
-Module
-~~~~~~
-
-.. autoclass:: pytest.Module()
-    :members:
-    :show-inheritance:
-
-Node
-~~~~
-
-.. autoclass:: _pytest.nodes.Node()
-    :members:
-
 Parser
 ~~~~~~
 
@@ -941,13 +966,6 @@ PytestPluginManager
     :inherited-members:
     :show-inheritance:
 
-Session
-~~~~~~~
-
-.. autoclass:: pytest.Session()
-    :members:
-    :show-inheritance:
-
 TestReport
 ~~~~~~~~~~
 
@@ -962,10 +980,10 @@ TestShortLogReport
 .. autoclass:: pytest.TestShortLogReport()
     :members:
 
-_Result
+Result
 ~~~~~~~
 
-Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`_Result in the pluggy documentation <pluggy._callers._Result>` for more information.
+Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`Result in the pluggy documentation <pluggy.Result>` for more information.
 
 Stash
 ~~~~~
@@ -1871,8 +1889,12 @@ All the command-line flags can be obtained by running ``pytest --help``::
                             tests. Optional argument: glob (default: '*').
       --cache-clear         Remove all cache contents at start of test run
       --lfnf={all,none}, --last-failed-no-failures={all,none}
-                            Which tests to run with no previously (known)
-                            failures
+                            With ``--lf``, determines whether to execute tests
+                            when there are no previously (known) failures or
+                            when no cached ``lastfailed`` data was found.
+                            ``all`` (the default) runs the full test suite
+                            again. ``none`` just emits a message about no known
+                            failures and exits successfully.
       --sw, --stepwise      Exit on test failure and continue from last failing
                             test next time
       --sw-skip, --stepwise-skip
@@ -1923,9 +1945,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
       --strict-markers      Markers not registered in the `markers` section of
                             the configuration file raise errors
       --strict              (Deprecated) alias to --strict-markers
-      -c, --config-file FILE
+      -c FILE, --config-file=FILE
                             Load configuration from `FILE` instead of trying to
-                            locate one of the implicit configuration files
+                            locate one of the implicit configuration files.
       --continue-on-collection-errors
                             Force test execution even if collection errors occur
       --rootdir=ROOTDIR     Define root directory for tests. Can be relative
diff --git a/scripts/prepare-release-pr.py b/scripts/prepare-release-pr.py
index 7a80de7edaa..8ffa6696466 100644
--- a/scripts/prepare-release-pr.py
+++ b/scripts/prepare-release-pr.py
@@ -31,10 +31,22 @@ class InvalidFeatureRelease(Exception):
 SLUG = "pytest-dev/pytest"
 
 PR_BODY = """\
-Created automatically from manual trigger.
+Created by the [prepare release pr]\
+(https://github.com/pytest-dev/pytest/actions/workflows/prepare-release-pr.yml) workflow.
 
-Once all builds pass and it has been **approved** by one or more maintainers, the build
-can be released by pushing a tag `{version}` to this repository.
+Once all builds pass and it has been **approved** by one or more maintainers, start the \
+[deploy](https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml) workflow, using these parameters:
+
+* `Use workflow from`: `release-{version}`.
+* `Release version`: `{version}`.
+
+Or execute on the command line:
+
+```console
+gh workflow run deploy.yml -r release-{version} -f version={version}
+```
+
+After the workflow has been approved by a core maintainer, the package will be uploaded to PyPI automatically.
 """
 
 
diff --git a/scripts/update-plugin-list.py b/scripts/update-plugin-list.py
index ea7e7986e15..2efe04165c2 100644
--- a/scripts/update-plugin-list.py
+++ b/scripts/update-plugin-list.py
@@ -13,14 +13,26 @@
 FILE_HEAD = r"""
 .. _plugin-list:
 
-Plugin List
-===========
+Pytest Plugin List
+==================
 
-PyPI projects that match "pytest-\*" are considered plugins and are listed
-automatically together with a manually-maintained list in `the source
-code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
+Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
+It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
 Packages classified as inactive are excluded.
 
+For detailed insights into how this list is generated,
+please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
+
+.. warning::
+
+   Please be aware that this list is not a curated collection of projects
+   and does not undergo a systematic review process.
+   It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins.
+
+   Do not presume any endorsement from the ``pytest`` project or its developers,
+   and always conduct your own quality assessment before incorporating any of these plugins into your own projects.
+
+
 .. The following conditional uses a different format for this list when
    creating a PDF, because otherwise the table gets far too wide for the
    page.
diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py
index ab83fee32b2..d1974bb3b4a 100644
--- a/src/_pytest/assertion/rewrite.py
+++ b/src/_pytest/assertion/rewrite.py
@@ -13,6 +13,7 @@
 import sys
 import tokenize
 import types
+from collections import defaultdict
 from pathlib import Path
 from pathlib import PurePath
 from typing import Callable
@@ -56,6 +57,10 @@
     astNum = ast.Num
 
 
+class Sentinel:
+    pass
+
+
 assertstate_key = StashKey["AssertionState"]()
 
 # pytest caches rewritten pycs in pycache dirs
@@ -63,6 +68,9 @@
 PYC_EXT = ".py" + (__debug__ and "c" or "o")
 PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
 
+# Special marker that denotes we have just left a scope definition
+_SCOPE_END_MARKER = Sentinel()
+
 
 class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
     """PEP302/PEP451 import hook which rewrites asserts."""
@@ -596,6 +604,13 @@ def _write_and_reset() -> None:
     return ret
 
 
+def _get_ast_constant_value(value: astStr) -> object:
+    if sys.version_info >= (3, 8):
+        return value.value
+    else:
+        return value.s
+
+
 class AssertionRewriter(ast.NodeVisitor):
     """Assertion rewriting implementation.
 
@@ -645,6 +660,8 @@ class AssertionRewriter(ast.NodeVisitor):
        .push_format_context() and .pop_format_context() which allows
        to build another %-formatted string while already building one.
 
+    :scope: A tuple containing the current scope used for variables_overwrite.
+
     :variables_overwrite: A dict filled with references to variables
        that change value within an assert. This happens when a variable is
        reassigned with the walrus operator
@@ -666,7 +683,10 @@ def __init__(
         else:
             self.enable_assertion_pass_hook = False
         self.source = source
-        self.variables_overwrite: Dict[str, str] = {}
+        self.scope: tuple[ast.AST, ...] = ()
+        self.variables_overwrite: defaultdict[
+            tuple[ast.AST, ...], Dict[str, str]
+        ] = defaultdict(dict)
 
     def run(self, mod: ast.Module) -> None:
         """Find all assert statements in *mod* and rewrite them."""
@@ -687,11 +707,10 @@ def run(self, mod: ast.Module) -> None:
                 expect_docstring
                 and isinstance(item, ast.Expr)
                 and isinstance(item.value, astStr)
+                and isinstance(_get_ast_constant_value(item.value), str)
             ):
-                if sys.version_info >= (3, 8):
-                    doc = item.value.value
-                else:
-                    doc = item.value.s
+                doc = _get_ast_constant_value(item.value)
+                assert isinstance(doc, str)
                 if self.is_rewrite_disabled(doc):
                     return
                 expect_docstring = False
@@ -732,9 +751,17 @@ def run(self, mod: ast.Module) -> None:
         mod.body[pos:pos] = imports
 
         # Collect asserts.
-        nodes: List[ast.AST] = [mod]
+        self.scope = (mod,)
+        nodes: List[Union[ast.AST, Sentinel]] = [mod]
         while nodes:
             node = nodes.pop()
+            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
+                self.scope = tuple((*self.scope, node))
+                nodes.append(_SCOPE_END_MARKER)
+            if node == _SCOPE_END_MARKER:
+                self.scope = self.scope[:-1]
+                continue
+            assert isinstance(node, ast.AST)
             for name, field in ast.iter_fields(node):
                 if isinstance(field, list):
                     new: List[ast.AST] = []
@@ -1005,7 +1032,7 @@ def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
                     ]
                 ):
                     pytest_temp = self.variable()
-                    self.variables_overwrite[
+                    self.variables_overwrite[self.scope][
                         v.left.target.id
                     ] = v.left  # type:ignore[assignment]
                     v.left.target.id = pytest_temp
@@ -1048,17 +1075,20 @@ def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]:
         new_args = []
         new_kwargs = []
         for arg in call.args:
-            if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite:
-                arg = self.variables_overwrite[arg.id]  # type:ignore[assignment]
+            if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
+                self.scope, {}
+            ):
+                arg = self.variables_overwrite[self.scope][
+                    arg.id
+                ]  # type:ignore[assignment]
             res, expl = self.visit(arg)
             arg_expls.append(expl)
             new_args.append(res)
         for keyword in call.keywords:
-            if (
-                isinstance(keyword.value, ast.Name)
-                and keyword.value.id in self.variables_overwrite
-            ):
-                keyword.value = self.variables_overwrite[
+            if isinstance(
+                keyword.value, ast.Name
+            ) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
+                keyword.value = self.variables_overwrite[self.scope][
                     keyword.value.id
                 ]  # type:ignore[assignment]
             res, expl = self.visit(keyword.value)
@@ -1094,12 +1124,14 @@ def visit_Attribute(self, attr: ast.Attribute) -> Tuple[ast.Name, str]:
     def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
         self.push_format_context()
         # We first check if we have overwritten a variable in the previous assert
-        if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
-            comp.left = self.variables_overwrite[
+        if isinstance(
+            comp.left, ast.Name
+        ) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
+            comp.left = self.variables_overwrite[self.scope][
                 comp.left.id
             ]  # type:ignore[assignment]
         if isinstance(comp.left, namedExpr):
-            self.variables_overwrite[
+            self.variables_overwrite[self.scope][
                 comp.left.target.id
             ] = comp.left  # type:ignore[assignment]
         left_res, left_expl = self.visit(comp.left)
@@ -1119,7 +1151,7 @@ def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
                 and next_operand.target.id == left_res.id
             ):
                 next_operand.target.id = self.variable()
-                self.variables_overwrite[
+                self.variables_overwrite[self.scope][
                     left_res.id
                 ] = next_operand  # type:ignore[assignment]
             next_res, next_expl = self.visit(next_operand)
diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py
index fc5dfdbd5ba..39ca5403e04 100644
--- a/src/_pytest/assertion/util.py
+++ b/src/_pytest/assertion/util.py
@@ -132,7 +132,7 @@ def isiterable(obj: Any) -> bool:
     try:
         iter(obj)
         return not istext(obj)
-    except TypeError:
+    except Exception:
         return False
 
 
diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py
index 855716d8199..1ecb8650580 100755
--- a/src/_pytest/cacheprovider.py
+++ b/src/_pytest/cacheprovider.py
@@ -505,7 +505,11 @@ def pytest_addoption(parser: Parser) -> None:
         dest="last_failed_no_failures",
         choices=("all", "none"),
         default="all",
-        help="Which tests to run with no previously (known) failures",
+        help="With ``--lf``, determines whether to execute tests when there "
+        "are no previously (known) failures or when no "
+        "cached ``lastfailed`` data was found. "
+        "``all`` (the default) runs the full test suite again. "
+        "``none`` just emits a message about no known failures and exits successfully.",
     )
 
 
diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py
index 352211de8aa..1d0add7363e 100644
--- a/src/_pytest/compat.py
+++ b/src/_pytest/compat.py
@@ -380,15 +380,24 @@ def __get__(self, instance, owner=None):
 
 
 def get_user_id() -> int | None:
-    """Return the current user id, or None if we cannot get it reliably on the current platform."""
-    # win32 does not have a getuid() function.
-    # On Emscripten, getuid() is a stub that always returns 0.
-    if sys.platform in ("win32", "emscripten"):
+    """Return the current process's real user id or None if it could not be
+    determined.
+
+    :return: The user id or None if it could not be determined.
+    """
+    # mypy follows the version and platform checking expectation of PEP 484:
+    # https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
+    # Containment checks are too complex for mypy v1.5.0 and cause failure.
+    if sys.platform == "win32" or sys.platform == "emscripten":
+        # win32 does not have a getuid() function.
+        # Emscripten has a return 0 stub.
         return None
-    # getuid shouldn't fail, but cpython defines such a case.
-    # Let's hope for the best.
-    uid = os.getuid()
-    return uid if uid != -1 else None
+    else:
+        # On other platforms, a return value of -1 is assumed to indicate that
+        # the current process's real user id could not be determined.
+        ERROR = -1
+        uid = os.getuid()
+        return uid if uid != ERROR else None
 
 
 # Perform exhaustiveness checking.
diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py
index c9a4b7f63cb..e3990d175df 100644
--- a/src/_pytest/config/__init__.py
+++ b/src/_pytest/config/__init__.py
@@ -57,6 +57,7 @@
 from _pytest.pathlib import import_path
 from _pytest.pathlib import ImportMode
 from _pytest.pathlib import resolve_package_path
+from _pytest.pathlib import safe_exists
 from _pytest.stash import Stash
 from _pytest.warning_types import PytestConfigWarning
 from _pytest.warning_types import warn_explicit_for
@@ -137,7 +138,9 @@ def main(
 ) -> Union[int, ExitCode]:
     """Perform an in-process test run.
 
-    :param args: List of command line arguments.
+    :param args:
+        List of command line arguments. If `None` or not given, defaults to reading
+        arguments directly from the process command line (:data:`sys.argv`).
     :param plugins: List of plugin objects to be auto-registered during initialization.
 
     :returns: An exit code.
@@ -442,10 +445,10 @@ def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):
         # so we avoid accessing possibly non-readable attributes
         # (see issue #1073).
         if not name.startswith("pytest_"):
-            return
+            return None
         # Ignore names which can not be hooks.
         if name == "pytest_plugins":
-            return
+            return None
 
         opts = super().parse_hookimpl_opts(plugin, name)
         if opts is not None:
@@ -454,9 +457,9 @@ def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):
         method = getattr(plugin, name)
         # Consider only actual functions for hooks (#3775).
         if not inspect.isroutine(method):
-            return
+            return None
         # Collect unmarked hooks as long as they have the `pytest_' prefix.
-        return _get_legacy_hook_marks(
+        return _get_legacy_hook_marks(  # type: ignore[return-value]
             method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
         )
 
@@ -465,7 +468,7 @@ def parse_hookspec_opts(self, module_or_class, name: str):
         if opts is None:
             method = getattr(module_or_class, name)
             if name.startswith("pytest_"):
-                opts = _get_legacy_hook_marks(
+                opts = _get_legacy_hook_marks(  # type: ignore[assignment]
                     method,
                     "spec",
                     ("firstresult", "historic"),
@@ -555,12 +558,8 @@ def _set_initial_conftests(
             anchor = absolutepath(current / path)
 
             # Ensure we do not break if what appears to be an anchor
-            # is in fact a very long option (#10169).
-            try:
-                anchor_exists = anchor.exists()
-            except OSError:  # pragma: no cover
-                anchor_exists = False
-            if anchor_exists:
+            # is in fact a very long option (#10169, #11394).
+            if safe_exists(anchor):
                 self._try_load_conftest(anchor, importmode, rootpath)
                 foundanchor = True
         if not foundanchor:
@@ -1063,9 +1062,10 @@ def _ensure_unconfigure(self) -> None:
             fin()
 
     def get_terminal_writer(self) -> TerminalWriter:
-        terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
+        terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
             "terminalreporter"
         )
+        assert terminalreporter is not None
         return terminalreporter._tw
 
     def pytest_cmdline_parse(
diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py
index 234b9e12906..02674ffae3b 100644
--- a/src/_pytest/config/findpaths.py
+++ b/src/_pytest/config/findpaths.py
@@ -16,6 +16,7 @@
 from _pytest.outcomes import fail
 from _pytest.pathlib import absolutepath
 from _pytest.pathlib import commonpath
+from _pytest.pathlib import safe_exists
 
 if TYPE_CHECKING:
     from . import Config
@@ -151,14 +152,6 @@ def get_dir_from_path(path: Path) -> Path:
             return path
         return path.parent
 
-    def safe_exists(path: Path) -> bool:
-        # This can throw on paths that contain characters unrepresentable at the OS level,
-        # or with invalid syntax on Windows (https://bugs.python.org/issue35306)
-        try:
-            return path.exists()
-        except OSError:
-            return False
-
     # These look like paths but may not exist
     possible_paths = (
         absolutepath(get_file_part_from_node_id(arg))
diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py
index 455ad62cc9c..ca41a98ea9c 100644
--- a/src/_pytest/doctest.py
+++ b/src/_pytest/doctest.py
@@ -1,5 +1,6 @@
 """Discover and run doctests in modules and test files."""
 import bdb
+import functools
 import inspect
 import os
 import platform
@@ -536,6 +537,25 @@ def _find(
                         tests, obj, name, module, source_lines, globs, seen
                     )
 
+            if sys.version_info < (3, 13):
+
+                def _from_module(self, module, object):
+                    """`cached_property` objects are never considered a part
+                    of the 'current module'. As such they are skipped by doctest.
+                    Here we override `_from_module` to check the underlying
+                    function instead. https://github.com/python/cpython/issues/107995
+                    """
+                    if hasattr(functools, "cached_property") and isinstance(
+                        object, functools.cached_property
+                    ):
+                        object = object.func
+
+                    # Type ignored because this is a private function.
+                    return super()._from_module(module, object)  # type: ignore[misc]
+
+            else:  # pragma: no cover
+                pass
+
         if self.path.name == "conftest.py":
             module = self.config.pluginmanager._importconftest(
                 self.path,
diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py
index af879aa44cf..d8c7e9fd3b6 100644
--- a/src/_pytest/faulthandler.py
+++ b/src/_pytest/faulthandler.py
@@ -1,4 +1,3 @@
-import io
 import os
 import sys
 from typing import Generator
@@ -10,8 +9,8 @@
 from _pytest.stash import StashKey
 
 
+fault_handler_original_stderr_fd_key = StashKey[int]()
 fault_handler_stderr_fd_key = StashKey[int]()
-fault_handler_originally_enabled_key = StashKey[bool]()
 
 
 def pytest_addoption(parser: Parser) -> None:
@@ -25,8 +24,15 @@ def pytest_addoption(parser: Parser) -> None:
 def pytest_configure(config: Config) -> None:
     import faulthandler
 
-    config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
-    config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
+    # at teardown we want to restore the original faulthandler fileno
+    # but faulthandler has no api to return the original fileno
+    # so here we stash the stderr fileno to be used at teardown
+    # sys.stderr and sys.__stderr__ may be closed or patched during the session
+    # so we can't rely on their values being good at that point (#11572).
+    stderr_fileno = get_stderr_fileno()
+    if faulthandler.is_enabled():
+        config.stash[fault_handler_original_stderr_fd_key] = stderr_fileno
+    config.stash[fault_handler_stderr_fd_key] = os.dup(stderr_fileno)
     faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
 
 
@@ -38,9 +44,10 @@ def pytest_unconfigure(config: Config) -> None:
     if fault_handler_stderr_fd_key in config.stash:
         os.close(config.stash[fault_handler_stderr_fd_key])
         del config.stash[fault_handler_stderr_fd_key]
-    if config.stash.get(fault_handler_originally_enabled_key, False):
-        # Re-enable the faulthandler if it was originally enabled.
-        faulthandler.enable(file=get_stderr_fileno())
+    # Re-enable the faulthandler if it was originally enabled.
+    if fault_handler_original_stderr_fd_key in config.stash:
+        faulthandler.enable(config.stash[fault_handler_original_stderr_fd_key])
+        del config.stash[fault_handler_original_stderr_fd_key]
 
 
 def get_stderr_fileno() -> int:
@@ -51,7 +58,7 @@ def get_stderr_fileno() -> int:
         if fileno == -1:
             raise AttributeError()
         return fileno
-    except (AttributeError, io.UnsupportedOperation):
+    except (AttributeError, ValueError):
         # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
         # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
         # This is potentially dangerous, but the best we can do.
diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py
index 430870608bd..ea16c438823 100644
--- a/src/_pytest/helpconfig.py
+++ b/src/_pytest/helpconfig.py
@@ -11,6 +11,7 @@
 from _pytest.config import ExitCode
 from _pytest.config import PrintHelp
 from _pytest.config.argparsing import Parser
+from _pytest.terminal import TerminalReporter
 
 
 class HelpAction(Action):
@@ -159,7 +160,10 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
 def showhelp(config: Config) -> None:
     import textwrap
 
-    reporter = config.pluginmanager.get_plugin("terminalreporter")
+    reporter: Optional[TerminalReporter] = config.pluginmanager.get_plugin(
+        "terminalreporter"
+    )
+    assert reporter is not None
     tw = reporter._tw
     tw.write(config._parser.optparser.format_help())
     tw.line()
diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py
index 9242d46d9df..9ee35b84e84 100644
--- a/src/_pytest/junitxml.py
+++ b/src/_pytest/junitxml.py
@@ -369,7 +369,7 @@ def test_foo(record_testsuite_property):
     __tracebackhide__ = True
 
     def record_func(name: str, value: object) -> None:
-        """No-op function in case --junitxml was not passed in the command-line."""
+        """No-op function in case --junit-xml was not passed in the command-line."""
         __tracebackhide__ = True
         _check_record_param_type("name", name)
 
@@ -502,6 +502,10 @@ def finalize(self, report: TestReport) -> None:
         # Local hack to handle xdist report order.
         workernode = getattr(report, "node", None)
         reporter = self.node_reporters.pop((nodeid, workernode))
+
+        for propname, propvalue in report.user_properties:
+            reporter.add_property(propname, str(propvalue))
+
         if reporter is not None:
             reporter.finalize()
 
@@ -599,9 +603,6 @@ def pytest_runtest_logreport(self, report: TestReport) -> None:
             reporter = self._opentestcase(report)
             reporter.write_captured_output(report)
 
-            for propname, propvalue in report.user_properties:
-                reporter.add_property(propname, str(propvalue))
-
             self.finalize(report)
             report_wid = getattr(report, "worker_id", None)
             report_ii = getattr(report, "item_index", None)
diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py
index 83813466016..9f2f1c79359 100644
--- a/src/_pytest/logging.py
+++ b/src/_pytest/logging.py
@@ -660,6 +660,8 @@ def __init__(self, config: Config) -> None:
         )
         if self._log_cli_enabled():
             terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
+            # Guaranteed by `_log_cli_enabled()`.
+            assert terminal_reporter is not None
             capture_manager = config.pluginmanager.get_plugin("capturemanager")
             # if capturemanager plugin is disabled, live logging still works.
             self.log_cli_handler: Union[
diff --git a/src/_pytest/main.py b/src/_pytest/main.py
index 155d4300e2c..ea89a63fa1b 100644
--- a/src/_pytest/main.py
+++ b/src/_pytest/main.py
@@ -36,6 +36,7 @@
 from _pytest.pathlib import absolutepath
 from _pytest.pathlib import bestrelpath
 from _pytest.pathlib import fnmatch_ex
+from _pytest.pathlib import safe_exists
 from _pytest.pathlib import visit
 from _pytest.reports import CollectReport
 from _pytest.reports import TestReport
@@ -462,6 +463,11 @@ def __missing__(self, path: Path) -> str:
 
 @final
 class Session(nodes.FSCollector):
+    """The root of the collection tree.
+
+    ``Session`` collects the initial paths given as arguments to pytest.
+    """
+
     Interrupted = Interrupted
     Failed = Failed
     # Set on the session by runner.pytest_sessionstart.
@@ -890,7 +896,7 @@ def resolve_collection_argument(
         strpath = search_pypath(strpath)
     fspath = invocation_path / strpath
     fspath = absolutepath(fspath)
-    if not fspath.exists():
+    if not safe_exists(fspath):
         msg = (
             "module or package not found: {arg} (missing __init__.py?)"
             if as_pypath
diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py
index 8dbff1dc93a..55620f0429e 100644
--- a/src/_pytest/mark/structures.py
+++ b/src/_pytest/mark/structures.py
@@ -373,7 +373,9 @@ def get_unpacked_marks(
         if not consider_mro:
             mark_lists = [obj.__dict__.get("pytestmark", [])]
         else:
-            mark_lists = [x.__dict__.get("pytestmark", []) for x in obj.__mro__]
+            mark_lists = [
+                x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__)
+            ]
         mark_list = []
         for item in mark_lists:
             if isinstance(item, list):
diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py
index dbd6b0a4273..a5313cb7656 100644
--- a/src/_pytest/nodes.py
+++ b/src/_pytest/nodes.py
@@ -157,10 +157,11 @@ def _create(self, *k, **kw):
 
 
 class Node(metaclass=NodeMeta):
-    """Base class for Collector and Item, the components of the test
-    collection tree.
+    r"""Base class of :class:`Collector` and :class:`Item`, the components of
+    the test collection tree.
 
-    Collector subclasses have children; Items are leaf nodes.
+    ``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are the
+    leaf nodes.
     """
 
     # Implemented in the legacypath plugin.
@@ -525,15 +526,17 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
 
 
 class Collector(Node):
-    """Collector instances create children through collect() and thus
-    iteratively build a tree."""
+    """Base class of all collectors.
+
+    Collector create children through `collect()` and thus iteratively build
+    the collection tree.
+    """
 
     class CollectError(Exception):
         """An error during collection, contains a custom message."""
 
     def collect(self) -> Iterable[Union["Item", "Collector"]]:
-        """Return a list of children (items and collectors) for this
-        collection node."""
+        """Collect children (items and collectors) for this collector."""
         raise NotImplementedError("abstract")
 
     # TODO: This omits the style= parameter which breaks Liskov Substitution.
@@ -564,7 +567,7 @@ def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
             ntraceback = traceback.cut(path=self.path)
             if ntraceback == traceback:
                 ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
-            return excinfo.traceback.filter(excinfo)
+            return ntraceback.filter(excinfo)
         return excinfo.traceback
 
 
@@ -577,6 +580,8 @@ def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[
 
 
 class FSCollector(Collector):
+    """Base class for filesystem collectors."""
+
     def __init__(
         self,
         fspath: Optional[LEGACY_PATH] = None,
@@ -660,7 +665,7 @@ class File(FSCollector):
 
 
 class Item(Node):
-    """A basic test invocation item.
+    """Base class of all test invocation items.
 
     Note that for a single function there might be multiple test invocation items.
     """
diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py
index 1be97dda4ea..53c3e1511cb 100644
--- a/src/_pytest/outcomes.py
+++ b/src/_pytest/outcomes.py
@@ -123,7 +123,7 @@ def exit(
         only because `msg` is deprecated.
 
     :param returncode:
-        Return code to be used when exiting pytest.
+        Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`.
 
     :param msg:
         Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py
index 70383e4b504..c2f8535f5f5 100644
--- a/src/_pytest/pathlib.py
+++ b/src/_pytest/pathlib.py
@@ -523,6 +523,8 @@ def import_path(
 
     if mode is ImportMode.importlib:
         module_name = module_name_from_path(path, root)
+        with contextlib.suppress(KeyError):
+            return sys.modules[module_name]
 
         for meta_importer in sys.meta_path:
             spec = meta_importer.find_spec(module_name, [str(path.parent)])
@@ -621,6 +623,11 @@ def module_name_from_path(path: Path, root: Path) -> str:
         # Use the parts for the relative path to the root path.
         path_parts = relative_path.parts
 
+    # Module name for packages do not contain the __init__ file, unless
+    # the `__init__.py` file is at the root.
+    if len(path_parts) >= 2 and path_parts[-1] == "__init__":
+        path_parts = path_parts[:-1]
+
     return ".".join(path_parts)
 
 
@@ -633,6 +640,9 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
     otherwise "src.tests.test_foo" is not importable by ``__import__``.
     """
     module_parts = module_name.split(".")
+    child_module: Union[ModuleType, None] = None
+    module: Union[ModuleType, None] = None
+    child_name: str = ""
     while module_name:
         if module_name not in modules:
             try:
@@ -642,13 +652,22 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
                 # ourselves to fall back to creating a dummy module.
                 if not sys.meta_path:
                     raise ModuleNotFoundError
-                importlib.import_module(module_name)
+                module = importlib.import_module(module_name)
             except ModuleNotFoundError:
                 module = ModuleType(
                     module_name,
                     doc="Empty module created by pytest's importmode=importlib.",
                 )
+        else:
+            module = modules[module_name]
+        if child_module:
+            # Add child attribute to the parent that can reference the child
+            # modules.
+            if not hasattr(module, child_name):
+                setattr(module, child_name, child_module)
                 modules[module_name] = module
+        # Keep track of the child module while moving up the tree.
+        child_module, child_name = module, module_name.rpartition(".")[-1]
         module_parts.pop(-1)
         module_name = ".".join(module_parts)
 
@@ -773,3 +792,13 @@ def copytree(source: Path, target: Path) -> None:
             shutil.copyfile(x, newx)
         elif x.is_dir():
             newx.mkdir(exist_ok=True)
+
+
+def safe_exists(p: Path) -> bool:
+    """Like Path.exists(), but account for input arguments that might be too long (#11394)."""
+    try:
+        return p.exists()
+    except (ValueError, OSError):
+        # ValueError: stat: path too long for Windows
+        # OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect
+        return False
diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py
index 3df52ebe88c..0771065e065 100644
--- a/src/_pytest/pytester.py
+++ b/src/_pytest/pytester.py
@@ -752,7 +752,7 @@ def preserve_module(name):
 
     def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder:
         """Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`."""
-        pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True)
+        pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True)  # type: ignore[attr-defined]
         self._request.addfinalizer(reprec.finish_recording)
         return reprec
 
@@ -1074,7 +1074,7 @@ def inline_runsource(self, source: str, *cmdlineargs) -> HookRecorder:
         return self.inline_run(*values)
 
     def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]:
-        """Run ``pytest.main(['--collectonly'])`` in-process.
+        """Run ``pytest.main(['--collect-only'])`` in-process.
 
         Runs the :py:func:`pytest.main` function to run all of pytest inside
         the test process itself like :py:meth:`inline_run`, but returns a
diff --git a/src/_pytest/python.py b/src/_pytest/python.py
index ad847c8afe2..5f8be5d9b9e 100644
--- a/src/_pytest/python.py
+++ b/src/_pytest/python.py
@@ -522,7 +522,7 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
 
 
 class Module(nodes.File, PyCollector):
-    """Collector for test classes and functions."""
+    """Collector for test classes and functions in a Python module."""
 
     def _getobj(self):
         return self._importtestmodule()
@@ -659,6 +659,9 @@ def _importtestmodule(self):
 
 
 class Package(Module):
+    """Collector for files and directories in a Python packages -- directories
+    with an `__init__.py` file."""
+
     def __init__(
         self,
         fspath: Optional[LEGACY_PATH],
@@ -788,7 +791,7 @@ def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[o
 
 
 class Class(PyCollector):
-    """Collector for test methods."""
+    """Collector for test methods (and nested classes) in a Python class."""
 
     @classmethod
     def from_parent(cls, parent, *, name, obj=None, **kw):
@@ -1149,7 +1152,7 @@ def setmulti(
         arg2scope = self._arg2scope.copy()
         for arg, val in zip(argnames, valset):
             if arg in params or arg in funcargs:
-                raise ValueError(f"duplicate {arg!r}")
+                raise ValueError(f"duplicate parametrization of {arg!r}")
             valtype_for_arg = valtypes[arg]
             if valtype_for_arg == "params":
                 params[arg] = val
@@ -1240,8 +1243,9 @@ def parametrize(
         during the collection phase. If you need to setup expensive resources
         see about setting indirect to do it rather than at test setup time.
 
-        Can be called multiple times, in which case each call parametrizes all
-        previous parametrizations, e.g.
+        Can be called multiple times per test function (but only on different
+        argument names), in which case each call parametrizes all previous
+        parametrizations, e.g.
 
         ::
 
@@ -1673,7 +1677,7 @@ def write_docstring(tw: TerminalWriter, doc: str, indent: str = "    ") -> None:
 
 
 class Function(PyobjMixin, nodes.Item):
-    """An Item responsible for setting up and executing a Python test function.
+    """Item responsible for setting up and executing a Python test function.
 
     :param name:
         The full function name, including any decorations like those
@@ -1830,10 +1834,8 @@ def repr_failure(  # type: ignore[override]
 
 
 class FunctionDefinition(Function):
-    """
-    This class is a step gap solution until we evolve to have actual function definition nodes
-    and manage to get rid of ``metafunc``.
-    """
+    """This class is a stop gap solution until we evolve to have actual function
+    definition nodes and manage to get rid of ``metafunc``."""
 
     def runtest(self) -> None:
         raise RuntimeError("function definitions are not supposed to be run as tests")
diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py
index 4213bd098c2..183356100c5 100644
--- a/src/_pytest/python_api.py
+++ b/src/_pytest/python_api.py
@@ -266,19 +266,20 @@ def _repr_compare(self, other_side: Mapping[object, float]) -> List[str]:
             approx_side_as_map.items(), other_side.values()
         ):
             if approx_value != other_value:
-                max_abs_diff = max(
-                    max_abs_diff, abs(approx_value.expected - other_value)
-                )
-                if approx_value.expected == 0.0:
-                    max_rel_diff = math.inf
-                else:
-                    max_rel_diff = max(
-                        max_rel_diff,
-                        abs(
-                            (approx_value.expected - other_value)
-                            / approx_value.expected
-                        ),
+                if approx_value.expected is not None and other_value is not None:
+                    max_abs_diff = max(
+                        max_abs_diff, abs(approx_value.expected - other_value)
                     )
+                    if approx_value.expected == 0.0:
+                        max_rel_diff = math.inf
+                    else:
+                        max_rel_diff = max(
+                            max_rel_diff,
+                            abs(
+                                (approx_value.expected - other_value)
+                                / approx_value.expected
+                            ),
+                        )
                 different_ids.append(approx_key)
 
         message_data = [
diff --git a/testing/_py/test_local.py b/testing/_py/test_local.py
index 348682b5396..fc20bd63b86 100644
--- a/testing/_py/test_local.py
+++ b/testing/_py/test_local.py
@@ -868,6 +868,9 @@ def test_fspath_protocol_other_class(self, fake_fspath_obj):
             py_path.strpath, str_path
         )
 
+    @pytest.mark.xfail(
+        reason="#11603", raises=(error.EEXIST, error.ENOENT), strict=False
+    )
     def test_make_numbered_dir_multiprocess_safe(self, tmpdir):
         # https://github.com/pytest-dev/py/issues/30
         with multiprocessing.Pool() as pool:
diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py
index 5658f2fd6b8..ed750a97071 100644
--- a/testing/acceptance_test.py
+++ b/testing/acceptance_test.py
@@ -1317,3 +1317,38 @@ def test_stuff():
     )
     res = pytester.runpytest()
     res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"])
+
+
+def test_doctest_and_normal_imports_with_importlib(pytester: Pytester) -> None:
+    """
+    Regression test for #10811: previously import_path with ImportMode.importlib would
+    not return a module if already in sys.modules, resulting in modules being imported
+    multiple times, which causes problems with modules that have import side effects.
+    """
+    # Uses the exact reproducer form #10811, given it is very minimal
+    # and illustrates the problem well.
+    pytester.makepyfile(
+        **{
+            "pmxbot/commands.py": "from . import logging",
+            "pmxbot/logging.py": "",
+            "tests/__init__.py": "",
+            "tests/test_commands.py": """
+                import importlib
+                from pmxbot import logging
+
+                class TestCommands:
+                    def test_boo(self):
+                        assert importlib.import_module('pmxbot.logging') is logging
+                """,
+        }
+    )
+    pytester.makeini(
+        """
+        [pytest]
+        addopts=
+            --doctest-modules
+            --import-mode importlib
+        """
+    )
+    result = pytester.runpytest_subprocess()
+    result.stdout.fnmatch_lines("*1 passed*")
diff --git a/testing/conftest.py b/testing/conftest.py
index 8e77fcae5ab..2a3ce5203d9 100644
--- a/testing/conftest.py
+++ b/testing/conftest.py
@@ -21,6 +21,15 @@ def restore_tracing():
             sys.settrace(orig_trace)
 
 
+@pytest.fixture(autouse=True)
+def set_column_width(monkeypatch: pytest.MonkeyPatch) -> None:
+    """
+    Force terminal width to 80: some tests check the formatting of --help, which is sensible
+    to terminal width.
+    """
+    monkeypatch.setenv("COLUMNS", "80")
+
+
 @pytest.hookimpl(hookwrapper=True, tryfirst=True)
 def pytest_collection_modifyitems(items):
     """Prefer faster tests.
diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py
index 8eaa2de96a8..753cf5fcd03 100644
--- a/testing/logging/test_fixture.py
+++ b/testing/logging/test_fixture.py
@@ -1,5 +1,7 @@
 # mypy: disable-error-code="attr-defined"
+# mypy: disallow-untyped-defs
 import logging
+from typing import Iterator
 
 import pytest
 from _pytest.logging import caplog_records_key
@@ -9,8 +11,8 @@
 sublogger = logging.getLogger(__name__ + ".baz")
 
 
-@pytest.fixture
-def cleanup_disabled_logging():
+@pytest.fixture(autouse=True)
+def cleanup_disabled_logging() -> Iterator[None]:
     """Simple fixture that ensures that a test doesn't disable logging.
 
     This is necessary because ``logging.disable()`` is global, so a test disabling logging
@@ -27,7 +29,7 @@ def test_fixture_help(pytester: Pytester) -> None:
     result.stdout.fnmatch_lines(["*caplog*"])
 
 
-def test_change_level(caplog):
+def test_change_level(caplog: pytest.LogCaptureFixture) -> None:
     caplog.set_level(logging.INFO)
     logger.debug("handler DEBUG level")
     logger.info("handler INFO level")
@@ -42,7 +44,7 @@ def test_change_level(caplog):
     assert "CRITICAL" in caplog.text
 
 
-def test_change_level_logging_disabled(caplog, cleanup_disabled_logging):
+def test_change_level_logging_disabled(caplog: pytest.LogCaptureFixture) -> None:
     logging.disable(logging.CRITICAL)
     assert logging.root.manager.disable == logging.CRITICAL
     caplog.set_level(logging.WARNING)
@@ -85,9 +87,7 @@ def test2(caplog):
     result.stdout.no_fnmatch_line("*log from test2*")
 
 
-def test_change_disabled_level_undo(
-    pytester: Pytester, cleanup_disabled_logging
-) -> None:
+def test_change_disabled_level_undo(pytester: Pytester) -> None:
     """Ensure that '_force_enable_logging' in 'set_level' is undone after the end of the test.
 
     Tests the logging output themselves (affected by disabled logging level).
@@ -144,7 +144,7 @@ def test3(caplog):
     result.assert_outcomes(passed=3)
 
 
-def test_with_statement(caplog):
+def test_with_statement(caplog: pytest.LogCaptureFixture) -> None:
     with caplog.at_level(logging.INFO):
         logger.debug("handler DEBUG level")
         logger.info("handler INFO level")
@@ -159,7 +159,7 @@ def test_with_statement(caplog):
     assert "CRITICAL" in caplog.text
 
 
-def test_with_statement_logging_disabled(caplog, cleanup_disabled_logging):
+def test_with_statement_logging_disabled(caplog: pytest.LogCaptureFixture) -> None:
     logging.disable(logging.CRITICAL)
     assert logging.root.manager.disable == logging.CRITICAL
     with caplog.at_level(logging.WARNING):
@@ -198,8 +198,8 @@ def test_with_statement_logging_disabled(caplog, cleanup_disabled_logging):
     ],
 )
 def test_force_enable_logging_level_string(
-    caplog, cleanup_disabled_logging, level_str, expected_disable_level
-):
+    caplog: pytest.LogCaptureFixture, level_str: str, expected_disable_level: int
+) -> None:
     """Test _force_enable_logging using a level string.
 
     ``expected_disable_level`` is one level below ``level_str`` because the disabled log level
@@ -218,7 +218,7 @@ def test_force_enable_logging_level_string(
     assert test_logger.manager.disable == expected_disable_level
 
 
-def test_log_access(caplog):
+def test_log_access(caplog: pytest.LogCaptureFixture) -> None:
     caplog.set_level(logging.INFO)
     logger.info("boo %s", "arg")
     assert caplog.records[0].levelname == "INFO"
@@ -226,7 +226,7 @@ def test_log_access(caplog):
     assert "boo arg" in caplog.text
 
 
-def test_messages(caplog):
+def test_messages(caplog: pytest.LogCaptureFixture) -> None:
     caplog.set_level(logging.INFO)
     logger.info("boo %s", "arg")
     logger.info("bar %s\nbaz %s", "arg1", "arg2")
@@ -247,14 +247,14 @@ def test_messages(caplog):
     assert "Exception" not in caplog.messages[-1]
 
 
-def test_record_tuples(caplog):
+def test_record_tuples(caplog: pytest.LogCaptureFixture) -> None:
     caplog.set_level(logging.INFO)
     logger.info("boo %s", "arg")
 
     assert caplog.record_tuples == [(__name__, logging.INFO, "boo arg")]
 
 
-def test_unicode(caplog):
+def test_unicode(caplog: pytest.LogCaptureFixture) -> None:
     caplog.set_level(logging.INFO)
     logger.info("bū")
     assert caplog.records[0].levelname == "INFO"
@@ -262,7 +262,7 @@ def test_unicode(caplog):
     assert "bū" in caplog.text
 
 
-def test_clear(caplog):
+def test_clear(caplog: pytest.LogCaptureFixture) -> None:
     caplog.set_level(logging.INFO)
     logger.info("bū")
     assert len(caplog.records)
@@ -273,7 +273,9 @@ def test_clear(caplog):
 
 
 @pytest.fixture
-def logging_during_setup_and_teardown(caplog):
+def logging_during_setup_and_teardown(
+    caplog: pytest.LogCaptureFixture,
+) -> Iterator[None]:
     caplog.set_level("INFO")
     logger.info("a_setup_log")
     yield
@@ -281,7 +283,9 @@ def logging_during_setup_and_teardown(caplog):
     assert [x.message for x in caplog.get_records("teardown")] == ["a_teardown_log"]
 
 
-def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardown):
+def test_caplog_captures_for_all_stages(
+    caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None
+) -> None:
     assert not caplog.records
     assert not caplog.get_records("call")
     logger.info("a_call_log")
@@ -290,25 +294,31 @@ def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardow
     assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
 
     # This reaches into private API, don't use this type of thing in real tests!
-    assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
+    caplog_records = caplog._item.stash[caplog_records_key]
+    assert set(caplog_records) == {"setup", "call"}
 
 
-def test_clear_for_call_stage(caplog, logging_during_setup_and_teardown):
+def test_clear_for_call_stage(
+    caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None
+) -> None:
     logger.info("a_call_log")
     assert [x.message for x in caplog.get_records("call")] == ["a_call_log"]
     assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
-    assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
+    caplog_records = caplog._item.stash[caplog_records_key]
+    assert set(caplog_records) == {"setup", "call"}
 
     caplog.clear()
 
     assert caplog.get_records("call") == []
     assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
-    assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
+    caplog_records = caplog._item.stash[caplog_records_key]
+    assert set(caplog_records) == {"setup", "call"}
 
     logging.info("a_call_log_after_clear")
     assert [x.message for x in caplog.get_records("call")] == ["a_call_log_after_clear"]
     assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
-    assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
+    caplog_records = caplog._item.stash[caplog_records_key]
+    assert set(caplog_records) == {"setup", "call"}
 
 
 def test_ini_controls_global_log_level(pytester: Pytester) -> None:
diff --git a/testing/python/approx.py b/testing/python/approx.py
index 631e52b56ac..6ad411a3e07 100644
--- a/testing/python/approx.py
+++ b/testing/python/approx.py
@@ -122,6 +122,23 @@ def test_error_messages_native_dtypes(self, assert_approx_raises_regex):
             ],
         )
 
+        assert_approx_raises_regex(
+            {"a": 1.0, "b": None, "c": None},
+            {
+                "a": None,
+                "b": 1000.0,
+                "c": None,
+            },
+            [
+                r"  comparison failed. Mismatched elements: 2 / 3:",
+                r"  Max absolute difference: -inf",
+                r"  Max relative difference: -inf",
+                r"  Index \| Obtained\s+\| Expected\s+",
+                rf"  a     \| {SOME_FLOAT} \| None",
+                rf"  b     \| None\s+\| {SOME_FLOAT} ± {SOME_FLOAT}",
+            ],
+        )
+
         assert_approx_raises_regex(
             [1.0, 2.0, 3.0, 4.0],
             [1.0, 3.0, 3.0, 5.0],
diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py
index a9e9b526934..dfb2dffb55e 100644
--- a/testing/python/metafunc.py
+++ b/testing/python/metafunc.py
@@ -1493,7 +1493,7 @@ def test_foo(x):
                 pass
         """
         )
-        result = pytester.runpytest("--collectonly")
+        result = pytester.runpytest("--collect-only")
         result.stdout.fnmatch_lines(
             [
                 "collected 0 items / 1 error",
diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py
index 778f843e6cf..788a0d4b455 100644
--- a/testing/test_assertrewrite.py
+++ b/testing/test_assertrewrite.py
@@ -686,6 +686,25 @@ def myany(x) -> bool:
         assert msg is not None
         assert "<MY42 object> < 0" in msg
 
+    def test_assert_handling_raise_in__iter__(self, pytester: Pytester) -> None:
+        pytester.makepyfile(
+            """\
+            class A:
+                def __iter__(self):
+                    raise ValueError()
+
+                def __eq__(self, o: object) -> bool:
+                    return self is o
+
+                def __repr__(self):
+                    return "<A object>"
+
+            assert A() == A()
+            """
+        )
+        result = pytester.runpytest()
+        result.stdout.fnmatch_lines(["*E*assert <A object> == <A object>"])
+
     def test_formatchar(self) -> None:
         def f() -> None:
             assert "%test" == "test"  # type: ignore[comparison-overlap]
@@ -1531,6 +1550,28 @@ def test_gt():
         result.stdout.fnmatch_lines(["*assert 4 > 5", "*where 5 = add_one(4)"])
 
 
+class TestIssue11239:
+    @pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason="Only Python 3.8+")
+    def test_assertion_walrus_different_test_cases(self, pytester: Pytester) -> None:
+        """Regression for (#11239)
+
+        Walrus operator rewriting would leak to separate test cases if they used the same variables.
+        """
+        pytester.makepyfile(
+            """
+            def test_1():
+                state = {"x": 2}.get("x")
+                assert state is not None
+
+            def test_2():
+                db = {"x": 2}
+                assert (state := db.get("x")) is not None
+        """
+        )
+        result = pytester.runpytest()
+        assert result.ret == 0
+
+
 @pytest.mark.skipif(
     sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
 )
@@ -2055,3 +2096,17 @@ def test_max_increased_verbosity(self, pytester: Pytester) -> None:
         self.create_test_file(pytester, DEFAULT_REPR_MAX_SIZE * 10)
         result = pytester.runpytest("-vv")
         result.stdout.no_fnmatch_line("*xxx...xxx*")
+
+
+class TestIssue11140:
+    def test_constant_not_picked_as_module_docstring(self, pytester: Pytester) -> None:
+        pytester.makepyfile(
+            """\
+            0
+
+            def test_foo():
+                pass
+            """
+        )
+        result = pytester.runpytest()
+        assert result.ret == 0
diff --git a/testing/test_collection.py b/testing/test_collection.py
index 8b0a1ab3650..5d4b0a75853 100644
--- a/testing/test_collection.py
+++ b/testing/test_collection.py
@@ -345,6 +345,29 @@ def pytest_make_collect_report():
         result = pytester.runpytest(p)
         result.stdout.fnmatch_lines(["*ERROR collecting*", "*header1*"])
 
+    def test_collection_error_traceback_is_clean(self, pytester: Pytester) -> None:
+        """When a collection error occurs, the report traceback doesn't contain
+        internal pytest stack entries.
+
+        Issue #11710.
+        """
+        pytester.makepyfile(
+            """
+            raise Exception("LOUSY")
+            """
+        )
+        result = pytester.runpytest()
+        result.stdout.fnmatch_lines(
+            [
+                "*ERROR collecting*",
+                "test_*.py:1: in <module>",
+                '    raise Exception("LOUSY")',
+                "E   Exception: LOUSY",
+                "*= short test summary info =*",
+            ],
+            consecutive=True,
+        )
+
 
 class TestCustomConftests:
     def test_ignore_collect_path(self, pytester: Pytester) -> None:
diff --git a/testing/test_doctest.py b/testing/test_doctest.py
index dfe569987ca..665bdb73b5d 100644
--- a/testing/test_doctest.py
+++ b/testing/test_doctest.py
@@ -482,6 +482,27 @@ def test_doctestmodule(self, pytester: Pytester):
         reprec = pytester.inline_run(p, "--doctest-modules")
         reprec.assertoutcome(failed=1)
 
+    @pytest.mark.skipif(
+        sys.version_info[:2] <= (3, 7), reason="Only Python 3.7 or less"
+    )
+    def test_doctest_cached_property(self, pytester: Pytester):
+        p = pytester.makepyfile(
+            """
+            import functools
+
+            class Foo:
+                @functools.cached_property
+                def foo(self):
+                    '''
+                    >>> assert False, "Tacos!"
+                    '''
+                    ...
+        """
+        )
+        result = pytester.runpytest(p, "--doctest-modules")
+        result.assert_outcomes(failed=1)
+        assert "Tacos!" in result.stdout.str()
+
     def test_doctestmodule_external_and_issue116(self, pytester: Pytester):
         p = pytester.mkpydir("hello")
         p.joinpath("__init__.py").write_text(
diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py
index 690830329c0..3f88c21e2b7 100644
--- a/testing/test_junitxml.py
+++ b/testing/test_junitxml.py
@@ -1228,6 +1228,36 @@ def test_record(record_property, other):
     result.stdout.fnmatch_lines(["*= 1 passed in *"])
 
 
+def test_record_property_on_test_and_teardown_failure(
+    pytester: Pytester, run_and_parse: RunAndParse
+) -> None:
+    pytester.makepyfile(
+        """
+        import pytest
+
+        @pytest.fixture
+        def other(record_property):
+            record_property("bar", 1)
+            yield
+            assert 0
+
+        def test_record(record_property, other):
+            record_property("foo", "<1")
+            assert 0
+    """
+    )
+    result, dom = run_and_parse()
+    node = dom.find_first_by_tag("testsuite")
+    tnodes = node.find_by_tag("testcase")
+    for tnode in tnodes:
+        psnode = tnode.find_first_by_tag("properties")
+        assert psnode, f"testcase didn't had expected properties:\n{tnode}"
+        pnodes = psnode.find_by_tag("property")
+        pnodes[0].assert_attr(name="bar", value="1")
+        pnodes[1].assert_attr(name="foo", value="<1")
+    result.stdout.fnmatch_lines(["*= 1 failed, 1 error *"])
+
+
 def test_record_property_same_name(
     pytester: Pytester, run_and_parse: RunAndParse
 ) -> None:
diff --git a/testing/test_main.py b/testing/test_main.py
index 71597626790..3c8998c1a35 100644
--- a/testing/test_main.py
+++ b/testing/test_main.py
@@ -262,3 +262,34 @@ def test(fix):
             "* 1 passed in *",
         ]
     )
+
+
+def test_very_long_cmdline_arg(pytester: Pytester) -> None:
+    """
+    Regression test for #11394.
+
+    Note: we could not manage to actually reproduce the error with this code, we suspect
+    GitHub runners are configured to support very long paths, however decided to leave
+    the test in place in case this ever regresses in the future.
+    """
+    pytester.makeconftest(
+        """
+        import pytest
+
+        def pytest_addoption(parser):
+            parser.addoption("--long-list", dest="long_list", action="store", default="all", help="List of things")
+
+        @pytest.fixture(scope="module")
+        def specified_feeds(request):
+            list_string = request.config.getoption("--long-list")
+            return list_string.split(',')
+        """
+    )
+    pytester.makepyfile(
+        """
+        def test_foo(specified_feeds):
+            assert len(specified_feeds) == 100_000
+        """
+    )
+    result = pytester.runpytest("--long-list", ",".join(["helloworld"] * 100_000))
+    result.stdout.fnmatch_lines("* 1 passed *")
diff --git a/testing/test_mark.py b/testing/test_mark.py
index e2d1a40c38a..2767260df8c 100644
--- a/testing/test_mark.py
+++ b/testing/test_mark.py
@@ -1130,6 +1130,41 @@ class C(A, B):
 
     all_marks = get_unpacked_marks(C)
 
-    assert all_marks == [xfail("c").mark, xfail("a").mark, xfail("b").mark]
+    assert all_marks == [xfail("b").mark, xfail("a").mark, xfail("c").mark]
 
     assert get_unpacked_marks(C, consider_mro=False) == [xfail("c").mark]
+
+
+# @pytest.mark.issue("https://github.com/pytest-dev/pytest/issues/10447")
+def test_mark_fixture_order_mro(pytester: Pytester):
+    """This ensures we walk marks of the mro starting with the base classes
+    the action at a distance fixtures are taken as minimal example from a real project
+
+    """
+    foo = pytester.makepyfile(
+        """
+        import pytest
+
+        @pytest.fixture
+        def add_attr1(request):
+            request.instance.attr1 = object()
+
+
+        @pytest.fixture
+        def add_attr2(request):
+            request.instance.attr2 = request.instance.attr1
+
+
+        @pytest.mark.usefixtures('add_attr1')
+        class Parent:
+            pass
+
+
+        @pytest.mark.usefixtures('add_attr2')
+        class TestThings(Parent):
+            def test_attrs(self):
+                assert self.attr1 == self.attr2
+        """
+    )
+    result = pytester.runpytest(foo)
+    result.assert_outcomes(passed=1)
diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py
index 1899abe153f..b6df035aacf 100644
--- a/testing/test_parseopt.py
+++ b/testing/test_parseopt.py
@@ -291,7 +291,8 @@ def test_multiple_metavar_help(self, parser: parseopt.Parser) -> None:
 
 def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
     try:
-        encoding = locale.getencoding()  # New in Python 3.11, ignores utf-8 mode
+        # New in Python 3.11, ignores utf-8 mode
+        encoding = locale.getencoding()  # type: ignore[attr-defined]
     except AttributeError:
         encoding = locale.getpreferredencoding(False)
     try:
diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py
index 56c54e484da..5ae0bcdde09 100644
--- a/testing/test_pathlib.py
+++ b/testing/test_pathlib.py
@@ -1,3 +1,4 @@
+import errno
 import os.path
 import pickle
 import sys
@@ -7,6 +8,7 @@
 from types import ModuleType
 from typing import Any
 from typing import Generator
+from typing import Iterator
 
 import pytest
 from _pytest.monkeypatch import MonkeyPatch
@@ -17,13 +19,16 @@
 from _pytest.pathlib import get_extended_length_path_str
 from _pytest.pathlib import get_lock_path
 from _pytest.pathlib import import_path
+from _pytest.pathlib import ImportMode
 from _pytest.pathlib import ImportPathMismatchError
 from _pytest.pathlib import insert_missing_modules
 from _pytest.pathlib import maybe_delete_a_numbered_dir
 from _pytest.pathlib import module_name_from_path
 from _pytest.pathlib import resolve_package_path
+from _pytest.pathlib import safe_exists
 from _pytest.pathlib import symlink_or_skip
 from _pytest.pathlib import visit
+from _pytest.pytester import Pytester
 from _pytest.tmpdir import TempPathFactory
 
 
@@ -282,29 +287,36 @@ def test_invalid_path(self, tmp_path: Path) -> None:
             import_path(tmp_path / "invalid.py", root=tmp_path)
 
     @pytest.fixture
-    def simple_module(self, tmp_path: Path) -> Path:
-        fn = tmp_path / "_src/tests/mymod.py"
+    def simple_module(
+        self, tmp_path: Path, request: pytest.FixtureRequest
+    ) -> Iterator[Path]:
+        name = f"mymod_{request.node.name}"
+        fn = tmp_path / f"_src/tests/{name}.py"
         fn.parent.mkdir(parents=True)
         fn.write_text("def foo(x): return 40 + x", encoding="utf-8")
-        return fn
+        module_name = module_name_from_path(fn, root=tmp_path)
+        yield fn
+        sys.modules.pop(module_name, None)
 
-    def test_importmode_importlib(self, simple_module: Path, tmp_path: Path) -> None:
+    def test_importmode_importlib(
+        self, simple_module: Path, tmp_path: Path, request: pytest.FixtureRequest
+    ) -> None:
         """`importlib` mode does not change sys.path."""
         module = import_path(simple_module, mode="importlib", root=tmp_path)
         assert module.foo(2) == 42  # type: ignore[attr-defined]
         assert str(simple_module.parent) not in sys.path
         assert module.__name__ in sys.modules
-        assert module.__name__ == "_src.tests.mymod"
+        assert module.__name__ == f"_src.tests.mymod_{request.node.name}"
         assert "_src" in sys.modules
         assert "_src.tests" in sys.modules
 
-    def test_importmode_twice_is_different_module(
+    def test_remembers_previous_imports(
         self, simple_module: Path, tmp_path: Path
     ) -> None:
-        """`importlib` mode always returns a new module."""
+        """`importlib` mode called remembers previous module (#10341, #10811)."""
         module1 = import_path(simple_module, mode="importlib", root=tmp_path)
         module2 = import_path(simple_module, mode="importlib", root=tmp_path)
-        assert module1 is not module2
+        assert module1 is module2
 
     def test_no_meta_path_found(
         self, simple_module: Path, monkeypatch: MonkeyPatch, tmp_path: Path
@@ -317,6 +329,9 @@ def test_no_meta_path_found(
         # mode='importlib' fails if no spec is found to load the module
         import importlib.util
 
+        # Force module to be re-imported.
+        del sys.modules[module.__name__]
+
         monkeypatch.setattr(
             importlib.util, "spec_from_file_location", lambda *args: None
         )
@@ -574,6 +589,14 @@ def test_module_name_from_path(self, tmp_path: Path) -> None:
         result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar"))
         assert result == "home.foo.test_foo"
 
+        # Importing __init__.py files should return the package as module name.
+        result = module_name_from_path(tmp_path / "src/app/__init__.py", tmp_path)
+        assert result == "src.app"
+
+        # Unless __init__.py file is at the root, in which case we cannot have an empty module name.
+        result = module_name_from_path(tmp_path / "__init__.py", tmp_path)
+        assert result == "__init__"
+
     def test_insert_missing_modules(
         self, monkeypatch: MonkeyPatch, tmp_path: Path
     ) -> None:
@@ -592,3 +615,100 @@ def test_insert_missing_modules(
         modules = {}
         insert_missing_modules(modules, "")
         assert modules == {}
+
+    def test_parent_contains_child_module_attribute(
+        self, monkeypatch: MonkeyPatch, tmp_path: Path
+    ):
+        monkeypatch.chdir(tmp_path)
+        # Use 'xxx' and 'xxy' as parent names as they are unlikely to exist and
+        # don't end up being imported.
+        modules = {"xxx.tests.foo": ModuleType("xxx.tests.foo")}
+        insert_missing_modules(modules, "xxx.tests.foo")
+        assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"]
+        assert modules["xxx"].tests is modules["xxx.tests"]
+        assert modules["xxx.tests"].foo is modules["xxx.tests.foo"]
+
+    def test_importlib_package(self, monkeypatch: MonkeyPatch, tmp_path: Path):
+        """
+        Importing a package using --importmode=importlib should not import the
+        package's __init__.py file more than once (#11306).
+        """
+        monkeypatch.chdir(tmp_path)
+        monkeypatch.syspath_prepend(tmp_path)
+
+        package_name = "importlib_import_package"
+        tmp_path.joinpath(package_name).mkdir()
+        init = tmp_path.joinpath(f"{package_name}/__init__.py")
+        init.write_text(
+            dedent(
+                """
+                from .singleton import Singleton
+
+                instance = Singleton()
+                """
+            ),
+            encoding="ascii",
+        )
+        singleton = tmp_path.joinpath(f"{package_name}/singleton.py")
+        singleton.write_text(
+            dedent(
+                """
+                class Singleton:
+                    INSTANCES = []
+
+                    def __init__(self) -> None:
+                        self.INSTANCES.append(self)
+                        if len(self.INSTANCES) > 1:
+                            raise RuntimeError("Already initialized")
+                """
+            ),
+            encoding="ascii",
+        )
+
+        mod = import_path(init, root=tmp_path, mode=ImportMode.importlib)
+        assert len(mod.instance.INSTANCES) == 1
+
+    def test_importlib_root_is_package(self, pytester: Pytester) -> None:
+        """
+        Regression for importing a `__init__`.py file that is at the root
+        (#11417).
+        """
+        pytester.makepyfile(__init__="")
+        pytester.makepyfile(
+            """
+            def test_my_test():
+                assert True
+            """
+        )
+
+        result = pytester.runpytest("--import-mode=importlib")
+        result.stdout.fnmatch_lines("* 1 passed *")
+
+
+def test_safe_exists(tmp_path: Path) -> None:
+    d = tmp_path.joinpath("some_dir")
+    d.mkdir()
+    assert safe_exists(d) is True
+
+    f = tmp_path.joinpath("some_file")
+    f.touch()
+    assert safe_exists(f) is True
+
+    # Use unittest.mock() as a context manager to have a very narrow
+    # patch lifetime.
+    p = tmp_path.joinpath("some long filename" * 100)
+    with unittest.mock.patch.object(
+        Path,
+        "exists",
+        autospec=True,
+        side_effect=OSError(errno.ENAMETOOLONG, "name too long"),
+    ):
+        assert safe_exists(p) is False
+
+    with unittest.mock.patch.object(
+        Path,
+        "exists",
+        autospec=True,
+        side_effect=ValueError("name too long"),
+    ):
+        assert safe_exists(p) is False
diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py
index c6f518b1da2..e5773412fbf 100644
--- a/testing/test_pluginmanager.py
+++ b/testing/test_pluginmanager.py
@@ -242,8 +242,12 @@ def test_consider_module(
         mod = types.ModuleType("temp")
         mod.__dict__["pytest_plugins"] = ["pytest_p1", "pytest_p2"]
         pytestpm.consider_module(mod)
-        assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1"
-        assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2"
+        p1 = pytestpm.get_plugin("pytest_p1")
+        assert p1 is not None
+        assert p1.__name__ == "pytest_p1"
+        p2 = pytestpm.get_plugin("pytest_p2")
+        assert p2 is not None
+        assert p2.__name__ == "pytest_p2"
 
     def test_consider_module_import_module(
         self, pytester: Pytester, _config_for_test: Config
@@ -336,6 +340,7 @@ def test_import_plugin_importname(
         len2 = len(pytestpm.get_plugins())
         assert len1 == len2
         plugin1 = pytestpm.get_plugin("pytest_hello")
+        assert plugin1 is not None
         assert plugin1.__name__.endswith("pytest_hello")
         plugin2 = pytestpm.get_plugin("pytest_hello")
         assert plugin2 is plugin1
@@ -351,6 +356,7 @@ def test_import_plugin_dotted_name(
         pluginname = "pkg.plug"
         pytestpm.import_plugin(pluginname)
         mod = pytestpm.get_plugin("pkg.plug")
+        assert mod is not None
         assert mod.x == 3
 
     def test_consider_conftest_deps(