How to Test Python Code

Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.

7 min read

Learning how to test Python code starts with a simple idea: write a separate program that calls your functions with known inputs and checks whether the output matches what you expect. Instead of running your script by hand and reading the terminal to decide if it worked, you write assertions that do the checking for you. Those assertions run every time you change your code and tell you immediately whether something broke.

A test is a function that calls another function and compares the result against an expected value. When the comparison fails, the framework prints the actual and expected values side by side. When it passes, you get a short confirmation and move on.

Run dozens of tests in seconds after every change. That speed catches mistakes before they reach a teammate.

Error handling with try and except blocks keeps your program running when problems occur. Testing prevents those problems from reaching users.

What a test looks like

The simplest possible test is a few lines of Python that call a function and check the return value. You can write this kind of check without any framework at all, just using Python's built-in assert statement.

pythonpython
def add(a, b):
    return a + b
 
result = add(2, 3)
assert result == 5, f"Expected 5, got {result}"

When the assertion is true, nothing visible happens and your script continues. When it is false, Python raises an AssertionError with your message and stops.

This raw assert pattern works for quick checks, but it has two problems. First, the script stops at the first failure, so you only learn about one bug at a time. Second, there is no summary at the end.

A proper test framework solves both problems. It runs every test independently, collects all results, and prints a report. Python's standard library includes the unittest module for this purpose. The third-party pytest framework is widely used for its simpler syntax.

The four parts of a test

Every automated test, regardless of the framework, has four parts. Understanding these parts helps you design tests that are clear, focused, and easy to debug when they fail.

  • Arrange: set up the data, objects, and conditions the test needs, such as creating a list or initializing a class instance.
  • Act: call the function or method you want to test.
  • Assert: check that the result matches your expectation.
  • Cleanup: restore any state the test changed, such as closing a file or removing a temporary record.

Here is that pattern written as a test function:

pythonpython
def test_average_of_three_numbers():
    # Arrange
    numbers = [4, 8, 12]
    expected = 8
 
    # Act
    result = sum(numbers) / len(numbers)
 
    # Assert
    assert result == expected

This is sometimes called Arrange-Act-Assert. If a test fails, you can look at the arrange section to see the inputs, the act section to see what was called, and the assert section to see what went wrong.

Python's built-in testing tools

Python ships with the unittest module, which gives you a test runner, assertion methods, and a way to group related tests into classes. You do not need to install anything extra to use it.

A test case in unittest is a class that inherits from unittest.TestCase. Each method whose name starts with test_ is a separate test that the framework discovers and runs automatically.

pythonpython
import unittest
 
def categorize_age(age):
    if age < 13:
        return "child"
    elif age < 20:
        return "teen"
    return "adult"
 
class TestCategorizeAge(unittest.TestCase):
    def test_child(self):
        self.assertEqual(categorize_age(8), "child")

The TestCase base class provides many assertion methods. The assertEqual method checks that two values match. Other methods like assertTrue and assertRaises handle boolean conditions and exception checking. The article on Python unittest explained covers the full set.

When you run this file, unittest discovers the test methods, runs each one, and prints a report. A passing run shows dots followed by OK. A failing run prints the exact assertion that failed and the mismatched values.

The pytest framework offers a lighter alternative to unittest. Instead of subclassing TestCase and calling assertion methods, you write plain functions using Python's built-in assert statement. pytest discovers test functions automatically by looking for files and functions that start with test_.

pythonpython
def add(a, b):
    return a + b
 
def test_add_positive_numbers():
    assert add(2, 3) == 5
 
def test_add_negative_numbers():
    assert add(-1, -1) == -2
 
def test_add_zero():
    assert add(5, 0) == 5

Run these tests with the pytest command in your terminal. pytest finds the file, runs all three functions, and reports the results. When an assertion fails, pytest shows the exact values involved in the comparison, which makes debugging faster than reading a generic error message.

pytest also includes a fixture system that replaces the setUp and tearDown methods from unittest with a more flexible pattern. Fixtures are functions that provide pre-built data or objects, and pytest handles the cleanup automatically. The article on writing better Python tests with pytest walks through fixtures and other pytest features step by step.

Tests as documentation

A well-written test suite does more than catch bugs. It serves as executable documentation for your code. When another developer, or your future self, wants to understand what a function is supposed to do, the tests show the expected inputs and outputs with concrete examples. Unlike comments, tests cannot go out of date without failing.

Consider a function that checks email format and the test that documents its expected behavior:

pythonpython
def is_valid_email(address):
    return "@" in address and "." in address.split("@")[-1]
 
def test_is_valid_email():
    assert is_valid_email("user@example.com") is True
    assert is_valid_email("user@example") is False
    assert is_valid_email("userexample.com") is False
    assert is_valid_email("") is False

The test immediately tells you what the function considers valid and invalid. If someone later changes the validation logic, they will see exactly which cases the existing code expects to pass.

Where tests live in a project

There is no single rule for where to put test files, but the Python community has settled on two common patterns. The first pattern places test files next to the code they test, so calculator.py lives alongside test_calculator.py in the same directory. The second and more common pattern creates a separate tests directory at the top level of the project.

texttext
my_project/
    calculator.py
    utils.py
    tests/
        test_calculator.py
        test_utils.py

The separate tests directory keeps production code clean and makes it easy to exclude tests when packaging or deploying. Both patterns work, and the article on organizing and running Python tests covers the tradeoffs for each layout.

Testing is a habit, not a phase

The most important idea in testing is that you do it continuously, not at the end. After writing a function that works, write one test that locks down the happy path. When a bug report arrives, write a test that reproduces the bug before fixing it. When you refactor, run the existing tests to confirm you did not break anything.

Start small. Pick one function in your current project, write a single test for it, and run that test. The rest of this section builds on that single habit.

Rune AI

Rune AI

Key Insights

  • Automated testing means writing code that checks your code, using assertions to compare actual output against expected output.
  • Python includes the unittest module in its standard library; pytest is a popular third-party alternative that uses plain assert statements.
  • A test case is a single scenario with a known input and an expected result; a test suite groups related test cases together.
  • Test fixtures handle setup and cleanup so every test starts from a known state.
  • Running tests after every change catches regressions early, when they are cheapest to fix.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between manual testing and automated testing in Python?

Manual testing means running your code by hand and checking the output with your eyes. Automated testing means writing a separate program that calls your code with known inputs and checks the results automatically using assertions. Automated tests run in seconds and can be repeated every time you change your code, which manual checking cannot match.

Do I need to install anything extra to write tests in Python?

No. Python includes the unittest module in its standard library, so you can write and run tests without installing any third-party packages. The pytest framework is a popular alternative that requires installation via pip, but it is not mandatory for getting started.

Should I write tests before or after writing my code?

Either approach works. Writing tests first (test-driven development) forces you to define what success looks like before you implement it. Writing tests after lets you explore the solution first and then lock down the behavior. Both approaches produce the same artifact: a test file that checks your code. Start with whichever approach feels more natural, and aim to have tests before you consider the code finished.

Conclusion

Testing is not a separate activity from programming. It is the part of programming that tells you whether your code actually works. Start with one assertion in one test file, run it, and build the habit of writing a test every time you fix a bug or add a feature.