Article summary

TL;DR

  • Install pytest and confirm the test runner is available.
  • Put the example function in shop.py and its tests in test_shop.py.
  • Use plain assert to describe the result you expect.
  • Run the tests with python -m pytest -v and read any failure before changing code.
  • Test ordinary values and the exact boundary where behavior changes.

01 / FIRST TEST

You only need one function to start testing with pytest.

If you can write and call a Python function, you already know enough Python to write your first automated test. You do not need a package, a web application, a test architecture, or a detailed understanding of testing theory.

You need one function, one expected result, and a way to check that the two agree.

Python gives you the assert statement for that check. pytest gives you a test runner that finds those checks, runs them, and explains what failed. The framework can handle much larger test suites later, but its smallest useful form is just another Python function.

A pytest test is a Python function whose name starts with `test_` and whose assertions describe the behavior you expect.

The whole example uses two files in one folder. Once it works, you will have the core feedback loop behind Python unit testing.

02 / SETUP

Set up a two-file Python example.

Open a terminal in a new folder. The official pytest getting-started guide begins with installing pytest. The command below uses the same Python launcher as your scripts:

python -m pip install -U pytest
python -m pytest --version

These commands use python as the Python launcher. If you normally run Python with py on Windows, use py in both commands instead.

The version command should print the installed pytest version. You do not need to create a Python package or add a configuration file for this example.

Create these two empty files in the folder:

first-pytest-test/
├── shop.py
└── test_shop.py

Put the function we want to test in shop.py:

def shipping_cost(order_total):
    if order_total >= 50:
        return 0
    return 5

The rule is intentionally small: orders of 50 or more get free shipping, and smaller orders cost 5. It has two possible results and one clear point where the behavior changes.

You can call it like any other function:

shipping_cost(75)  # returns 0
shipping_cost(20)  # returns 5

Trying the function manually is useful while you are learning. It is not an automated test, though. You have to remember the correct result and inspect it yourself every time. A test records the expectation and checks it whenever you run the suite.

03 / WRITE THE TEST

Write your first test with plain assert.

Add this code to test_shop.py:

from shop import shipping_cost


def test_shipping_is_free_for_large_orders():
    order_total = 75
    cost = shipping_cost(order_total)

    assert cost == 0

This test follows a simple sequence:

  1. Arrange: choose an order total of 75.
  2. Act: call shipping_cost().
  3. Assert: check that the returned cost is 0.

The blank lines are not required by pytest. They make the three parts easier to see while you are learning.

The name test_shipping_is_free_for_large_orders matters. pytest discovers functions whose names begin with test_. The rest of the name explains the behavior. A descriptive name is much more useful than test_1 when a larger suite fails.

You do not need to import pytest for this first test. assert is built into Python. When pytest runs the file, it adds useful detail to a failed assertion so you can compare the actual and expected values.

A test function does not need to return a result. It passes when it finishes without raising an error and all its assertions are true. It fails when an assertion is false or another unexpected exception occurs.

04 / RUN PYTEST

Let pytest find and run the test.

Run this command from the folder that contains both files:

python -m pytest -v

-v means verbose. It asks pytest to show each discovered test by name. The important part of the output should look similar to this:

collected 1 item

test_shop.py::test_shipping_is_free_for_large_orders PASSED

pytest found the test because both names follow its default discovery conventions:

  • the file is named test_shop.py;
  • the function is named test_shipping_is_free_for_large_orders.

You can run only this file by passing its path:

python -m pytest test_shop.py -v

For this two-file example, both commands run the same test. The first command becomes more useful once the folder contains several test files.

If pytest reports no tests ran, check the basics before changing any configuration:

  1. Are you running the command from the folder containing test_shop.py?
  2. Does the test filename begin with test_?
  3. Does the test function name begin with test_?
  4. Did you save the file before running pytest?

That is all test discovery you need for the first example. Custom discovery rules and test directories can wait until your projects actually need them.

05 / READ FAILURES

A failing test tells you which expectation broke.

A test that always passes does not teach you much about the failure report. Make this test fail on purpose by changing the expected shipping cost from 0 to 5:

def test_shipping_is_free_for_large_orders():
    order_total = 75
    cost = shipping_cost(order_total)

    assert cost == 5

Run the test again:

python -m pytest test_shop.py -v

pytest now marks the test as FAILED and highlights the assertion. The central line will show the mismatch:

E       assert 0 == 5

Pytest output showing the same shipping test passing and then failing because the function returned 0 while the test expected 5

The function returned 0, but the test expected 5. In this case the function is correct and we deliberately wrote the wrong expectation. In real work, a failure can mean one of three things:

  • the production code is wrong;
  • the test expectation is wrong;
  • your understanding of the requirement is incomplete.

Do not automatically change the assertion just to make the test green. First decide which behavior is supposed to be true.

Restore assert cost == 0 and run the test once more. It should pass again. You have now used the basic testing loop: run the test, inspect the failure, correct the problem, and rerun it.

06 / TEST THE BOUNDARY

Test the place where behavior changes.

The first test proves that an order of 75 gets free shipping. It does not prove that the threshold of 50 works correctly.

The most interesting values are often near a boundary. For this function, the behavior changes between 49 and 50. Add one test on each side:

from shop import shipping_cost


def test_shipping_is_free_for_large_orders():
    assert shipping_cost(75) == 0


def test_shipping_costs_five_below_free_shipping_threshold():
    assert shipping_cost(49) == 5


def test_shipping_is_free_at_threshold():
    assert shipping_cost(50) == 0

Run the full file:

python -m pytest test_shop.py -v

You should see three passing tests.

These cases have different jobs. The value 75 is an ordinary free-shipping order. The values 49 and 50 protect the exact rule where paid shipping becomes free. If someone accidentally changes >= 50 to > 50, the test at 50 will fail.

You do not need to test every possible order total. Choose a small set of examples that represent distinct behavior:

  1. one normal case;
  2. the value immediately below a boundary;
  3. the value exactly on the boundary.

This is already more useful than testing arbitrary values. Good tests are not a contest to create the most assertions. They record behavior that would matter if it changed.

07 / BEGINNER QUESTIONS

Clear up the common first-test questions.

Do I need to import pytest in every test file?

No. Tests that only use normal Python code and plain assert do not need import pytest. You import pytest when you use framework features such as pytest.mark.parametrize or pytest.raises.

Is this a unit test?

Yes. The test calls one small function directly and does not use a database, network request, filesystem, or another external service. The useful idea is the boundary, not the label: you give the function an input and check its observable result.

Is pytest part of Python?

pytest is a separate testing framework, which is why you installed it. Your production function remains ordinary Python. Your first test also uses Python’s own assert statement, while pytest handles discovery, execution, and the failure report.

Is there a downside to starting with pytest?

pytest is an extra tool to install, and its discovery rules can be confusing until you know the naming conventions. Advanced fixtures and plugins can also add complexity when they are introduced before a project needs them. None of that changes the small starting point in this tutorial: one function, one assertion, and one command.

Should every Python function have its own test?

Not automatically. Protect meaningful behavior rather than chasing a one-to-one relationship between functions and tests. A tiny helper that is fully exercised through a public function may not need a separate test. A business rule such as the free-shipping threshold deserves direct coverage because a small change affects the result users receive.

Why can pytest import shop.py here?

Both files are in the same folder, and you run pytest from that folder. This keeps the first example simple. When your code grows into a package with nested directories, you should use a proper project layout, but you do not need one to learn the testing loop.

08 / NEXT STEP

Keep going after the first useful test.

You now know the core pytest loop: choose one behavior, express the expected result with assert, run the test, and use a failure as feedback. That same loop remains useful when the code grows beyond one function.

The next topics are usually fixtures, exceptions, parametrization, mocking, API tests, coverage, and automated test runs in CI. They solve real problems, but learning them all before your first test would hide the simple idea underneath.

Continue with the complete Pytest course and learn fixtures, mocking, API testing, coverage, and GitHub Actions step by step.

View course

One advanced trick: run the same test with several cases

The three shipping tests are easy to read. When many cases check the same rule, @pytest.mark.parametrize can remove repetition without hiding the inputs:

import pytest

from shop import shipping_cost


@pytest.mark.parametrize(
    "order_total,expected_cost",
    [
        (0, 5),
        (49, 5),
        (50, 0),
        (75, 0),
    ],
)
def test_shipping_cost(order_total, expected_cost):
    assert shipping_cost(order_total) == expected_cost

pytest runs this function four times, once for each input and expected result. Here you do import pytest because the decorator belongs to the framework.

Do not rush to parametrize every test. Separate test functions are often clearer when cases represent different behaviors or need different setup. Use parametrization when the operation is the same and the table of examples makes the rule easier to scan.

If you are introducing the same testing loop to other developers, the next problem is usually practice rather than syntax. I cover that transition in How to Onboard a Python Team to Testing with Pytest.

For one final experiment, temporarily change >= 50 to > 50 in shop.py. Run the tests and identify the single case that catches the bug. Then restore >= and rerun the suite. That short exercise shows exactly why a well-chosen boundary test earns its place.