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:

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
  1. Initialization Hooks
  1. Collection Hooks
  1. Test running (runtest) hooks
  1. Reporting Hooks
  1. Debugging/Interaction Hooks

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.

Collection Hooks

Some popular collection hooks are:

Test Running (runtest) Hooks

Here are some of the popular test running hooks:

Reporting Hooks

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

Debugging/Interaction Hooks

Here are some of the popular debugging/interaction hooks:

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.:

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:

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