Back to Blog
Python

Python Pytest: Basic Tests, Assertions, and Test Discovery

python pytest basic tests assertions and test discovery: Learn how to write basic pytest tests with plain assert statements, understand pytest test discovery rules, an...

pytestpython testingassertionstest discoveryunit testing
A pytest test file with assert statements being collected and run by the pytest runner.

pytest's basic workflow is deliberately small: you write plain functions, use the assert statement for checks, and pytest finds your tests by name. Understanding python pytest basic tests assertions and test discovery comes down to three things: how a test function is recognized, what happens when an assertion fails, and which files and functions pytest collects when you run it from the command line.

Writing the First Test Function

A test is an ordinary function whose name starts with test_. There is no base class and no required decorator.

def test_addition(): assert 1 + 1 == 2

The assert statement is the assertion. When the expression evaluates to True, the test passes. When it evaluates to False, the test fails. This works for any expression, not just equality:

def test_string_contains(): result = "hello world" assert "world" in result def test_list_has_item(): items = ["a", "b", "c"] assert "b" in items

Keep test names descriptive because the name is what pytest shows in output and what you use to select tests later.

How pytest Discovers Tests

When you run pytest without arguments, it starts in the current directory and walks down through subdirectories. A file is collected only if its name matches test_*.py or *_test.py. Inside a collected file, functions matching test_* are collected, and classes matching Test* are collected along with their methods matching test_*.

ElementDefault patternExample
Filetest_*.py or *_test.pytest_math.py, math_test.py
Functiontest_*test_addition
ClassTest*TestMath
Methodtest_*test_division

To see exactly what pytest would collect without running anything, use pytest --collect-only. This is the fastest way to debug why a test is not being found.

Assertions and How pytest Reports Failures

pytest rewrites the bytecode of test modules at import time so that a failed assert prints the actual values involved.

def test_comparison(): expected = {"status": 200, "body": "ok"} actual = {"status": 404, "body": "ok"} assert actual == expected

When this fails, pytest shows a diff that highlights which keys differ instead of a bare AssertionError. This is why plain assert is the recommended style in pytest; you do not need the assertEqual or assertTrue methods from unittest.

Testing Exceptions and Floating-Point Values

Exceptions are checked with pytest.raises:

import pytest def test_raises(): with pytest.raises(ValueError): int("not-a-number")

The block passes if the specified exception is raised and fails otherwise. You can also inspect the exception object:

def test_raises_with_message(): with pytest.raises(ValueError) as exc_info: int("not-a-number") assert "invalid literal" in str(exc_info.value)

Floating-point equality is fragile, so use pytest.approx:

def test_float(): assert 0.1 + 0.2 == pytest.approx(0.3)

pytest.approx compares values within a relative tolerance instead of requiring exact equality.

Grouping Tests in Classes

pytest also collects methods named test_* inside classes named Test*:

class TestUser: def test_create(self): assert True def test_delete(self): assert True

The class must not define __init__, because pytest instantiates the class once per test method. Classes are useful for grouping related tests and for class-scoped fixtures. For simple suites, plain functions avoid the extra ceremony.

Running a Subset of Tests

Filter by keyword with -k:

pytest -k "user and not slow"

Run a single test by node ID:

pytest tests/test_user.py::TestUser::test_create

Use -v to print each test name, and -x to stop at the first failure while debugging. The exit code tells you the outcome at a glance:

CodeMeaning
0All tests passed
1Some tests failed
2Interrupted by the user
3Internal error
4Usage error
5No tests collected

Common Discovery Pitfalls and Isolation

A file named helpers.py will not be collected even if it contains functions named test_*, because the filename does not match the discovery pattern. Likewise, a function named check_result inside a test_*.py file is ignored. Classes with an __init__ method raise an error during collection.

Test isolation works per function: state created in one test does not leak into another. Module-level state does persist across tests in the same file, so avoid mutating module globals inside tests. When several tests need the same setup, use a fixture instead of module-level code; fixtures give each test a fresh instance and keep cleanup explicit.

Keep test names descriptive and keep each test focused on one behavior. When a test fails, the name and the assertion diff should be enough to locate the fault without reading the whole module.

python pytest basic tests assertions and test discovery: Pra | RYUSLOG DEV