Introduction To Pytest Hooks (A Practical Guide For Beginners)

Have you ever wondered how to customize the behaviour of your test suite in Pytest?

Perhaps you want to set a configfile path or read it, before executing your tests?

Maybe set some global variables for Dev or Prod or send Slack notifications after the test suite has finished running?

How do you achieve this in Pytest?

Well, you can do all this and more using Pytest Hooks.

Pytest hooks are intervention points within the Pytest framework, allowing you to plug in or alter the default behaviour of the testing process.

You can control Pytest’s behaviour at various stages of the test lifecycle, such as before test collection, after test execution, or when exceptions are raised.

They’re different from Pytest fixtures in the sense that they’re not directly called by tests but invoked automatically based on test events.

Hooks work alongside plugins to extend Pytest’s functionality.

Think of them as strategically placed entry points or “hooks” where you can insert your own code to add or alter functionality.

In this comprehensive tutorial, you’ll learn everything you need to know about using Pytest Hooks and how to use some of the most popular ones with examples.

So buckle up, and let’s begin!

Link To GitHub Repo

What You’ll Learn

By the end of this article, you should be able to:

  • grasp the basics of Pytest hooks and their uses.
  • understand different types of Pytest hooks to judge which one to use when.
  • learn about the ordering of Pytest hooks.
  • gain insights into how hooks, plugins and fixtures are different and interlinked at the same time.
  • implement hooks yourself.

What are Pytest Hooks?

Hooks are a key part of Pytest’s plugin system used to extend Pytest’s functionality.

At their core, Pytest Hooks are gateway points, allowing you to inject logic at specific stages of the test execution process to modify or extend the behaviour of tests based on test events.

Pytest offers a variety of hooks, each designed for different purposes and stages of the test cycle.

We’ll look at practical examples soon but first let’s understand the different types of hooks and where they sit within Pytest.

There are too many, but here are some of the most commonly used hooks:

Types of Pytest Hooks

Pytest Hooks are categorized based on the stage of the testing process they’re involved in.

  1. Bootstrapping Hooks
  • Called at the very beginning and end of the Pytest test run. Crucial for setting up and tearing down configurations or environments that are needed for the entire test suite.
  1. Initialization Hooks
  • These hooks come into play after bootstrapping and are instrumental for tasks like adding command-line options or integrating new plugins. They set the stage for a customized testing environment.
  1. Collection Hooks
  • These hooks deal with discovering and organizing test cases. They give you the power to influence how Pytest collects tests, allowing you to add, modify, or skip tests based on custom criteria.
  1. Test running (runtest) hooks
  • These hooks offer a way to customize test execution. They’re incredibly versatile, enabling actions before, during, and after a test is run. This can range from setting up test data to cleaning up resources after a test.
  1. Reporting Hooks
  • These hooks are all about how test results are processed and presented. They provide a means to customize the output, create custom reports, or even integrate with external systems for reporting purposes.
  1. Debugging/Interaction Hooks
  • These hooks are invaluable for debugging. They come into play when tests fail or when you need to drop into an interactive session. They can help in inspecting the state of a test at various points or managing breakpoints..

Now that we’ve skimmed the surface of Pytest Hooks, let’s dive deeper into the different types of hooks and their applications.

Bootstrapping Hooks

Here are some examples of the most commonly used bootstrapping hooks, pulled straight from the documentation:

  • pytest_load_initial_conftests - Called when initial conftest files are loaded.

  • pytest_cmdline_parse - Called after the command line has been parsed but before any further action is taken.

  • pytest_cmdline_main - Called after command line options have been parsed and all plugins and initial conftest files have been loaded.

Initialization Hooks

Following are some examples of the most commonly used initialization hooks.

  • pytest_addoption - Called when command line options are parsed. It allows plugins to register new command line options or modify existing ones. Example Guide.

  • pytest_configure - This hook allows you to perform custom configuration of the test environment like pass in a config parameter or share global variables.

  • pytest_unconfigure - Called before test execution exits. It allows plugins to perform custom cleanup actions.

  • pytest_addhooks - Called when plugins are initialized. It allows plugins to register new hooks or modify existing ones.

  • pytest_sessionstart - Called after the pytest session object has been created and before performing collection and entering the run test loop.

  • pytest_sessionfinish - Called after the test session has finished. It allows plugins to perform custom actions after all tests have been run.

  •  pytest_plugin_registered - Called when a plugin is registered. It allows plugins to perform custom actions when other plugins are registered.

Collection Hooks

Some popular collection hooks are:

  • pytest_collection - Perform the collection phase for the given session.

  • pytest_ignore_collect - Allows plugins to ignore certain items during collection.

  • pytest_collect_directory - Allows plugins to modify the collection of directories.

  • pytest_collect_file - Allows plugins to modify the collection of files.

  • pytest_generate_tests - Allows plugins to dynamically generate tests or modify existing ones based on Pytest Parametrize. Here’s a more detailed guide on pytest_generate_tests and how to use it to dynamically generate tests.

  • pytest_make_parametrize_id - Allows plugins to customize the test ids as part of Pytest Parametrization.

  • pytest_collection_modifyitems - It lets you modify the collected test items (test functions, classes, etc.) before the actual test run. You can use this hook to dynamically filter or reorder tests.

  • pytest_collection_finish - Allows plugins to perform custom actions after collection has finished.

Test Running (runtest) Hooks

Here are some of the popular test running hooks:

  • pytest_runtestloop - Allows plugins to perform custom actions before the test loop starts.

  • pytest_runtest_protocol - Performs the runtest protocol for the given test item - setup, call, and teardown.

  • pytest_runtest_logstart - Called at the start of running the runtest protocol for a test.

  • pytest_runtest_logfinish - Called at the end of running the runtest protocol for a test.

  • pytest_runtest_setup - Called at the start of running the setup phase of the runtest protocol for a test.

  • pytest_runtest_call - Called at the start of running the call phase of the runtest protocol for a test.

  • pytest_runtest_teardown - Called at the start of running the teardown phase of the runtest protocol for a test.

  • pytest_runtest_makereport - Called to create a test report for the given test item.

Reporting Hooks

OK now, let’s look at some of the popular reporting hooks which can be instrumental for your test reporting needs:

  • pytest_collectstart - Called at the start of collection.

  • pytest_make_collect_report - Called to create a collection report for the given item.

  • pytest_itemcollected - Called after an item has been collected.

  • pytest_collectreport - Called to create a collection report for the given item.

  • pytest_deselected - Called when a test item has been deselected e.g. by keyword.

  • pytest_report_header - Called to create a report header.

  • pytest_report_collectionfinish - Called after collection has finished.

  • pytest_report_teststatus - Called to report the status of a test.

  • pytest_report_to_serializable - Called to convert a report to a serializable format e.g. to JSON.

  • pytest_report_from_serializable - Called to read a serializable report back from JSON e.g. from JSON.

  • pytest_terminal_summary - Called at the end of the test session to print a summary of the test run.

  • pytest_fixture_setup - Called at the start of setting up a fixture.

  • pytest_fixture_post_finalizer - Called after fixture teardown but before the cache is cleared.

  • pytest_warning_recorded - Called when a warning is recorded.

  • pytest_runtest_logreport - Called to report the result of a test.

  • pytest_assertrepr_compare - Called to create a custom assertion representation. Gives you an explanation for comparisons in failing assert expressions.

  • pytest_assertion_pass - Called when an assertion passes.

Debugging/Interaction Hooks

Here are some of the popular debugging/interaction hooks:

  • pytest_internalerror - Called when a Pytest internal error occurs.

  • pytest_keyboard_interrupt - Called when a keyboard interrupt occurs.

  • pytest_exception_interact - Called when an exception is raised during test execution and can likely be interactively handled.

  • pytest_enter_pdb - Called when entering the Python debugger.

  • pytest_leave_pdb - Called when leaving the Python debugger.

OK that was a mouthful, but I hope you now have some understanding of the scope of different types of Pytest hooks and their applications.

Now let’s loook into how to actually use Pytest Hooks.

Project Set Up

Getting Started

The project has the following structure - a simple repo with a test file and conftest.py.

1
2
3
4
5
6
7
8
.
├── .gitignore
├── README.md
├── requirements.txt
└── tests
└── example1
├── conftest.py
└── test_example1.py

Prerequisites

You don’t need to know everything but some basics of Python and Pytest would be helpful.:

  • Python (3.11+)
  • Pytest

To get started, clone the repo here and install any dependencies into your virtual environment via pip.

Example - pytest_sessionstart and pytest_sessionfinish Hooks

Let’s look at how to use 2 simple hooks - pytest_sessionstart and pytest_sessionfinish - to perform some custom actions at the start and end of the test session.

In your conftest.py file, let’s define these hooks.

1
2
3
4
5
6
7
8
9
10
11
12
13
import pytest


@pytest.hookimpl()
def pytest_sessionstart(session):
print("Hello from `pytest_sessionstart` hook!")


@pytest.hookimpl()
def pytest_sessionfinish(session, exitstatus):
print("Hello from `pytest_sessionfinish` hook!")
print(f"Exit status: {exitstatus}")

You’ll notice that I’ve used the @pytest.hookimpl() decorator to mark these functions as hook implementations. This is a way of telling Pytest that these functions are implementations of hooks.

Let’s also define a simple test for the sake of this example.

./tests/example1/test_example1.py

1
2
3
def test_example1_pass():
print("Running test1")
assert True

Running the tests will now show the output of the hooks.

Note the print statement before “—– test session starts —–” and after PASSED at the end of the test session.

pytest_sessionstart and pytest_sessionfinish

Pretty cool, right?

Example - pytest_runtest_makereport Hook

Having looked at the session hooks, let’s take a look at how to use one of the reporting hooks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_makereport(item, call: CallInfo):
# Let's ensure we are dealing with a test report
if call.when == "call":
outcome = call.excinfo

try:
# Access the test outcome (passed, failed, etc.)
test_outcome = "failed" if outcome else "passed"
# Access the test duration
test_duration = call.duration
# Access the test ID (nodeid)
test_id = item.nodeid

# Print Test Outcome and Duration
print(f"Test: {test_id}")
print(f"Test Outcome: {test_outcome}")
print(f"Test Duration: {test_duration:.5f} seconds")
except Exception as e:
print("ERROR:", e)

Let’s break this down:

  • The goal of this hook is to print out the outcome and duration of each test after it has been run.
  • We’ve used the @pytest.hookimpl(tryfirst=True) decorator to ensure that this hook is executed before other hooks.
  • The pytest_runtest_makereport hook takes 2 arguments - item and call. The item argument represents the test item, and the call argument represents the call information.
  • We check if the call is for the “call” phase of the test, and if so, we access the test outcome, duration, and ID and print them out.
  • The call.excinfo attribute is specifically an exception info object for failed tests. call.excinfo being None typically indicates the test passed or was skipped, but it’s primary purpose is to provide details on exceptions.
  • We can get the test node id using item.nodeid.

Running this yields

pytest_runtest_makereport

These are just a couple of basic items but you can see how powerful these hooks can be.

Ordering of Pytest Hooks

Not only do Pytest Hooks allow us cool customization options, but also control the order in which these are executed.

You can do this via the @pytest.hookimpl decorator.

The @pytest.hookimpl decorator is used to mark a function as a hook implementation.

  1. Execution as Early as Possible
    By using tryfirst=True as an argument, you can make a hook implementation execute earlier than others.
1
2
3
4
# Plugin 1
@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(items):
# This code executes early
  1. Execution as Late as Possible
    Alternatively, trylast=True ensures the hook implementation executes later in the sequence.
1
2
3
4
# Plugin 2
@pytest.hookimpl(trylast=True)
def pytest_collection_modifyitems(items):
# This code executes later
  1. Hook Wrappers
    The hookwrapper=True argument creates a hook that wraps around all others. It can execute code both before and after the standard hooks.
1
2
3
4
5
6
# Plugin 3
@pytest.hookimpl(hookwrapper=True)
def pytest_collection_modifyitems(items):
# Code here executes before all non-wrapper hooks
outcome = yield
# Code here executes after all non-wrapper hooks

Now, let’s see the order in which the hooks from the above examples would execute:

1- Plugin 3’s pytest_collection_modifyitems executes up to the yield statement (since it’s a wrapper).
2- Plugin 1’s pytest_collection_modifyitems executes next due to its tryfirst=True.
3- Plugin 2’s pytest_collection_modifyitems follows. It’s marked with trylast=True, but even without this, it would execute after Plugin 1.
4- Plugin 3’s pytest_collection_modifyitems resumes after the yield, completing the wrapper hook.

Understanding this ordering mechanism allows you to strategically customize hook implementations in Pytest.

This ensures that your testing workflow accommodates multiple plugins and complex scenarios effectively.

Plugins vs Hooks

As you learnt, hooks are predefined points in the framework’s execution at which you can insert your own code to alter or enhance the testing process.

For example, you might use a hook to add log statements before and after test execution or to modify test items before they are run.

Plugins, on the other hand, are external or internal extensions that utilize these hooks to integrate additional features into Pytest.

Plugins can range from adding support for parallel test execution, generating detailed reports, to integrating with other tools and frameworks.

Check out our article on the 8 best Pytest plugins to supercharge your testing workflow.

In essence, plugins are a way to package and distribute custom hooks and fixtures, making them reusable across different projects.

Fixtures vs Hooks

Maybe you’re wondering, what about fixtures?

Don’t they do the same thing as some of the Pytest hooks?

Pytest Fixtures are reusable pieces of code that you can invoke in your tests to set up a specific state or environment before the test runs and optionally clean up after the test is done.

These important operations are commonly referred to as setup and teardown.

This could include preparing database connections, creating test data, or configuring system states.

Fixtures help in maintaining a clean, DRY (Don’t Repeat Yourself) codebase, making tests easier to write and understand.

While fixtures primarily focus on setting up and tearing down test conditions, hooks give you the leverage to customize the testing process itself.

Together, they help you tailor Pytest to your specific needs.

Functionality:

  • Hooks are used to alter the framework’s behavior and react to various testing events.
  • Fixtures are used for setting up and tearing down test environments and states.

Invocation:

  • Hooks are invoked automatically by Pytest based on certain events in the test lifecycle.
  • Fixtures are invoked explicitly by naming them as parameters in tests or other fixtures, or by setting the autouse=True flag.

Scope and Impact:

  • Hooks have a more global impact, influencing the overall behaviour of the test suite.
  • Fixtures have a localized impact, managing the environment for specific tests or groups of tests, controlled by the scope parameter.

FAQs

Do Pytest Hooks Have Any Significant Performance Implications on My Test Suite?

Pytest hooks generally do not have significant performance implications on your test suite.

However, as with any code, If a hook contains complex or resource-intensive logic, obviously, it will slow down the testing process.

Avoid heavy computations or I/O operations in hooks that are called frequently, as it will lead to noticeable overhead.

Use hooks sparingly, where necessary, and keep them as lightweight as possible. You should be good to go.

Can Pytest Hooks be Defined Outside conftest.py?

Defining your hooks in conftest.py is indeed common practice.

Simple coz of the fact that conftest.py is a special file that Pytest recognizes and uses to collect and apply hooks and fixtures across the entire test suite.

For projects with a complex structures, having multiple conftest.py files in different subdirectories can help manage scope and customization more effectively.

However, you can absolutely define Pytest hooks outside of conftest.py.

You can define hooks in any Python file and then register them using the pytest.hookimpl() decorator.

Now if you really want to share the logic acrross multiple projects, you can package your hooks as a plugin and distribute it via PyPI.

But that’s a topic for another day.

Can I Use Pytest Hooks to Dynamically Generate Tests based on Certain Conditions?

The primary hook for this magic trick is pytest_generate_tests.

This hook is called when collecting a test function, allowing you to modify or generate test calls dynamically.

Lucky for you, we’ve already written a detailed guide on how to do this. Check it out here.

How Do I Access Command Line Arguments Passed to Pytest Within a Hook?

Accessing command line arguments in a Pytest hook is pretty straightforward and quite handy, especially when you need to customize test behaviour based on those arguments.

All you need to use is the pytest_addoption hook to add your own custom command line arguments to Pytest.

This article goes into detail on how to do this.

Can I Use Pytest Hooks to Share Global Variables Between Tests?

Well, the answer is yes and recommendation is no. While you can share global variables using hooks, it not a good practice unless fully necessary.

This is because it can lead to test dependencies and make your test suite harder to maintain and debug. Ensuring test isolation is a key principle of good test design.

That said, you can always use the pytest_configure hook to share global variables (not states) across tests - for example a workspace_id or a config file path.

This article on Python Unit Testing Best Practices can help you design your suite in the best way from the get-go.

Session-scoped fixtures can also help with this.

How To Access All Collected Test Reports in Pytest?

This is a question asked by a fellow developer on StackOverflow who wanted to send logs over to Datadog.

Now this is a great example of the use case of reporting hooks, for example, pytest_terminal_summary.

This hook is invoked at the end of the test session and allows you to interact with the test reports that have been collected over the session.

It’s like getting a comprehensive summary of the entire test run, which you can then use to generate custom reports or perform analysis.

You can read more about how to use it here.

Conclusion

OK time to wrap up.

In this article we covered a lot on Pytest hooks.

We saw why hooks are useful, listed some of the most popular Pytest hooks, based on category and their applications, and even looked at how to use them in practice.

We then learnt how they differ from plugins and fixtures and how they can be used to customize the testing process.

We also discussed the ordering of hooks and how to control their execution sequence. Lastly we tackled a few common questions about Pytest hooks.

From now on, don’t be afraid to experiment with Pytest hooks and use them to customize your test suite to your heart’s content.

If you have ideas for improvement or like me to cover anything specific, please send me a message via Twitter, GitHub or Email.

Till the next time… Cheers!

Additional Reading

Link to Example Code Repo
What Are Pytest Fixture Scopes? (How To Choose The Best Scope For Your Test)
Python Unit Testing Best Practices For Building Reliable Applications
How To Use Pytest With Command Line Options (Easy To Follow Guide)
What is Setup and Teardown in Pytest? (Importance of a Clean Test Environment)
How Pytest Fixtures Can Help You Write More Readable And Efficient Tests
8 Useful Pytest Plugins To Make Your Python Unit Tests Easier, Faster and Prettier
How to Effortlessly Generate Unit Test Cases with Pytest Parameterized Tests
A Beginner’s Guide To pytest_generate_tests (Explained With 2 Examples)
What Is The pytest_configure Hook? (A Simple Guide)
Official Docs - pytest Writing Hooks
Official Docs - pytest Hooks
Understanding Hooks in pytest