Pytest markers are a powerful way to categorize and filter your test cases, letting you run only specific subsets of your test suite.
Let’s see this in action. Imagine you have a set of tests for different parts of your application:
# test_api.py
import pytest
@pytest.mark.api
def test_user_creation():
assert True
@pytest.mark.api
@pytest.mark.slow
def test_complex_report_generation():
assert True
# test_database.py
import pytest
@pytest.mark.database
def test_user_save():
assert True
@pytest.mark.database
def test_user_delete():
assert True
# test_ui.py
import pytest
@pytest.mark.ui
def test_login_form():
assert True
With these markers, you can now selectively run tests.
First, you need to register your markers to avoid warnings. Create a pytest.ini file in your project’s root directory:
# pytest.ini
[pytest]
markers =
api: tests related to the API layer.
database: tests related to the database layer.
ui: tests related to the UI layer.
slow: marks tests that are expected to be slow.
Now, let’s run some commands. To run all tests marked api:
pytest -m api
This will execute test_user_creation and test_complex_report_generation.
To run tests that are both api and slow:
pytest -m "api and slow"
This will only run test_complex_report_generation.
You can also exclude markers. To run all tests except those marked database:
pytest -m "not database"
This will run all your api and ui tests, but skip the database ones.
The core problem markers solve is test suite management. As your project grows, running the entire test suite before every commit can become prohibitively slow. Markers allow you to create logical groupings (like "smoke tests," "integration tests," "performance tests") and execute only the relevant subset for a given context. For instance, a developer working on the API might only run pytest -m api, while a QA engineer preparing for a release might run pytest -m "not slow".
Internally, pytest collects all your test functions. When it encounters a marker, it associates that tag with the test item. During test collection, pytest builds an internal representation of your test graph. The -m flag then acts as a filter on this graph, selecting only the nodes (test items) that match the provided expression. The expression language supports and, or, and not operators, allowing for complex filtering logic.
The actual mechanism for applying markers is a decorator, @pytest.mark.<marker_name>. Pytest inspects these decorators during test collection. If a marker isn’t registered in pytest.ini, pytest will warn you, which is a good practice to catch typos or unannounced marker usage. You can also pass multiple -m arguments, which are implicitly combined with or. So, pytest -m api -m ui is equivalent to pytest -m "api or ui".
A common pitfall is forgetting to register markers in pytest.ini. Without registration, pytest will issue warnings for every unknown marker, and while it still runs the tests, it makes your test output noisy and can mask other important warnings. Another subtle point is that markers are just strings. There’s no inherent type checking or validation beyond what you define in pytest.ini for documentation purposes. You could technically have @pytest.mark.api and @pytest.mark.API and pytest would treat them as entirely different markers unless you explicitly register both or use a consistent naming convention.
The next step in managing your tests is often exploring fixtures, which allow you to set up and tear down test environments and provide dependencies to your test functions.