Test Python Functions with Assertions
Write repeatable tests that compare actual results with expected results and cover boundaries.
Video lesson: Test Python Functions with Assertions
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
Suppose you write a small function to keep a value between zero and ten. If the value is five, the result should stay five. If it is twelve, the result should become ten. If it is negative two, the result should become zero. That sounds simple enough to check by printing one example. But one example can be a trap: a function that returns its input unchanged will look correct when the input already lies in the range. In this video we will run that deliberately broken function, add tests that distinguish it from the correct behavior, repair it, and run a small standard-library test suite. The companion lesson has a browser console and an automatically checked task so you can try the same cases.
- 00:46
A good test starts with an independent expected result. If we decide what should happen only after we see the program's output, it is easy to mistake an incorrect output for a plausible one. Keep the three expected answers in mind while the terminal runs. Before writing tests, decide whether low and high are inclusive and what reversed bounds mean. Those details are part of the function's contract, not implementation trivia. Two developers can write different functions that both look reasonable until a boundary or invalid range reveals the disagreement. State the expectation first. The first line prints true.
- 01:26
If that were our only test, we might believe clamp is done. But the function is merely returning its input. Twelve stays twelve, outside our upper bound of ten. Notice that printing the number tells us what happened but does not itself say that the result is wrong. The expected answer came from the contract, not from the implementation. To make this repeatable, replace visual checking with assertions. An assertion compares an actual value with the expected one. If the comparison is false, Python raises AssertionError. This is useful for tests and debugging. It is not a good way to validate untrusted application input, because Python can omit assert statements when run with optimization.
- 02:11
For actual program errors, use explicit checks and raise a suitable exception. Now let us write enough assertions to expose the no-op implementation. A human can miss a wrong printed value during a long run, especially when many examples scroll by. A test makes the discrepancy stop the run and names the case that failed. We keep the broken result visible here so the later passing suite has a meaningful before-and-after comparison. Both outside-range tests fail, exactly where we expected. These failures are evidence about the missing branches. The correct function needs to return the lower bound when the input is below it, the upper bound when the input is above it, and the input itself otherwise.
- 02:57
Values exactly on the boundaries should remain unchanged because our interval is inclusive. We also need a rule for bad bounds: if low is greater than high, the interval makes no sense. We will raise ValueError for that condition. Test design is often about finding plausible wrong implementations. If we only test the middle, a no-op function passes. If we only test a high value, a function that always returns ten might pass. A useful small suite has an inside value, a low outside value, a high outside value, both exact boundaries, and the invalid-range path. Those few cases tell us much more than a large number of nearly identical middle values.
- 03:41
There is another tempting wrong implementation: always return the nearest upper bound. It would pass one above-range test but fail the middle and below-range cases. Tests become stronger when they cover distinct behaviors rather than repeating the same path with slightly different numbers. Include the error path because it is part of the requirement. The corrected function checks the invalid interval first. For a valid interval, the two comparisons handle values outside the bounds; the final return handles values inside, including the exact low and high values. The output now matches the three predictions from the opening.
- 04:22
We could write the normal case as `min(max(value, low), high)`, but explicit branches make the contract easy to trace while learning. Notice that return sends a value to the caller. The function itself does not need to print. This makes it reusable in a web page, a command-line program, or another function, and makes tests straightforward. Our checks so far are simple assertions. The standard library also provides unittest to group and report tests. We can use it without installing any dependency in the browser console or on a normal Python installation. The order of the checks matters.
- 05:02
Reject reversed bounds before comparing the value to either bound; otherwise a contradictory range can return something that looks numeric but has no sensible meaning. The final return is reached only after the two outside cases have been ruled out. Trace one input through each branch. unittest discovers methods whose names begin with `test_`. `assertEqual` compares actual and expected values, while `assertRaises` checks that the invalid interval produces ValueError. The test runner returns a result object; we print the number of tests and whether all succeeded. Three passing tests do not prove every possible input is correct, but they give repeatable evidence for three distinct behaviors.
- 05:48
If a user later reports a bug at a boundary, add that input as a new test before changing the function. On PythonLessonLab, the task checker tests the middle, both directions outside the interval, the boundaries, negative bounds, and reversed bounds. Run the starter code first and read which checks fail. Then implement one branch at a time, run again, and finish the quiz. For a follow-up, create a `safe_percentage` function and write tests for zero, one hundred, and values outside that range. Make each expected value clear before executing the code. When a test fails, read which case failed and compare the actual value with the expected one.
- 06:30
Do not merely change the expected value until the suite turns green; revisit the contract. A regression test is most useful when it captures a real failure and remains in place after the repair.
Understand the concept
A test calls a function with a chosen input and checks an expected result. A printed number requires a human to notice whether it is correct; an assertion makes the comparison executable. assert actual == expected raises AssertionError when the condition is false. In production applications, use a test framework such as unittest or pytest to organize tests, rather than relying on assertions for input validation.
Pick examples that would reveal plausible bugs. For a function that clamps a value to a range, test a value inside the range, one below it, and one above it. Test the exact lower and upper boundaries too. A function that works for one comfortable example can still fail at an edge.
The standard-library unittest module groups test methods in a TestCase. Methods whose names begin with test_ are collected as tests. assertEqual reports the actual and expected values when they differ. The browser guide runs a suite with an in-memory result stream so you can focus on the outcome without installing a package.
The practice task implements clamp(value, low, high). Its checker tries several values and a reversed range. Raising ValueError for low greater than high prevents a nonsensical result. The test is useful only when its expected outcome is chosen independently of the implementation, so read the requirement before you look at the code.
- assert checks an expectation
- Test the middle and both boundaries
- unittest groups named tests
- Tests should expose plausible wrong implementations
See it step by step
Read the code, predict the output, then compare it with the result.
01. Make an expectation executable
def double(value):
return value * 2
assert double(4) == 8
print("test passed")test passedThe assertion would stop the program with AssertionError if double(4) did not equal 8.
02. Test a normal case and boundaries
def clamp(value, low, high):
if low > high:
raise ValueError("low must not exceed high")
return min(max(value, low), high)
assert clamp(5, 0, 10) == 5
assert clamp(-1, 0, 10) == 0
assert clamp(11, 0, 10) == 10
print("three cases passed")three cases passedThe three cases exercise distinct paths: unchanged, raised to the lower bound, and lowered to the upper bound.
03. Run a tiny unittest suite
import unittest
from io import StringIO
def double(value):
return value * 2
class DoubleTests(unittest.TestCase):
def test_zero(self):
self.assertEqual(double(0), 0)
def test_negative(self):
self.assertEqual(double(-3), -6)
suite = unittest.defaultTestLoader.loadTestsFromTestCase(DoubleTests)
result = unittest.TextTestRunner(stream=StringIO(), verbosity=0).run(suite)
print(result.testsRun, result.wasSuccessful())2 Trueunittest discovers the two test_ methods and returns a result with the number run and whether all passed.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Make the expected result part of the program
A print statement can show a result, but somebody still has to compare it with the requirement. An assertion makes the expected result executable. If the comparison is true, Python continues silently. If it is false, Python raises AssertionError. In the example, a deliberately broken doubling function returns value plus two instead of value times two. It passes for two, which shows why one convenient input does not prove a function is correct.
Adding a second input makes the defect visible. This is a useful way to design tests: imagine a plausible wrong implementation and choose a case that distinguishes it from the correct one. For production test suites, unittest or another framework organizes failures. Also remember that assertions may be removed when Python runs with optimization, so they are for tests and debugging rather than validating untrusted user input in application code.
def broken_double(value):
return value + 2
print(broken_double(2) == 4)
try:
assert broken_double(4) == 8
except AssertionError:
print("second test failed")True
second test failed- The wrong function happens to work when the input is 2.
- Input 4 separates addition from multiplication.
- A failing assertion turns that mismatch into a repeatable signal.
Test every branch and the boundary
The requirement for clamp has three normal paths: return the lower bound, return the upper bound, or return the value itself. A reversed range is an error path. A test suite should cover each path and the exact boundaries. Only testing a value inside the range would allow a no-op implementation that always returns its input. A below-range and above-range value expose that mistake immediately.
Boundary values also tell you whether the interval is inclusive. Here a value exactly equal to low or high remains that value. The function checks low > high before looking at value; otherwise it could return a number from a nonsensical range. Think of the tests as examples of the contract. They are not a substitute for a clear contract: decide what reversed bounds mean first, then assert that behavior.
def clamp(value, low, high):
if low > high:
raise ValueError("low must not exceed high")
return min(max(value, low), high)
cases = [(-1, 0), (0, 0), (5, 5), (10, 10), (11, 10)]
for value, expected in cases:
assert clamp(value, 0, 10) == expected
print(f"{len(cases)} cases passed")5 cases passed- The first and last cases test values outside the interval.
- The two exact bounds and one middle value test the inclusive interval.
- Change the function to return value and observe which cases fail.
Group related checks with unittest
The standard library includes unittest, so this example works without installing pytest. A TestCase subclass groups related checks. Method names beginning with test_ are recognized by the loader. assertEqual reports the actual and expected values if they differ, which makes the failure more informative than a bare print. The TextTestRunner can write to an in-memory stream; the example prints only a compact summary so the expected output is stable in the browser console.
A passing suite means these chosen cases passed, not that every possible input is correct. Keep extending tests when a bug exposes a missing edge case. For clamp, reversed bounds deserve an assertRaises test, while an empty collection would matter for a different function. The website task checks several separate cases so you can see which requirement is still unmet. Try the failing starter code first; then implement the function and run the checks again.
import unittest
from io import StringIO
def clamp(value, low, high):
if low > high:
raise ValueError("low must not exceed high")
return min(max(value, low), high)
class ClampTests(unittest.TestCase):
def test_below(self):
self.assertEqual(clamp(-1, 0, 10), 0)
def test_invalid_bounds(self):
with self.assertRaises(ValueError):
clamp(5, 10, 0)
suite = unittest.defaultTestLoader.loadTestsFromTestCase(ClampTests)
result = unittest.TextTestRunner(stream=StringIO(), verbosity=0).run(suite)
print(result.testsRun, result.wasSuccessful())2 True- The loader finds methods whose names begin with test_.
- assertRaises checks the error path without allowing it to stop the suite.
- The result reports two tests run and both successful.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Check low > high first and raise ValueError. Then use if value < low and if value > high; return value when neither condition applies.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Printing a result without checking it
print(double(4))assert double(4) == 8What happens: The terminal shows a value, but a wrong value does not fail a repeatable test.
State the expected value explicitly so a regression fails when the test runs.
Testing only the easiest case
assert clamp(5, 0, 10) == 5assert clamp(5, 0, 10) == 5
assert clamp(-1, 0, 10) == 0
assert clamp(11, 0, 10) == 10What happens: An implementation that always returns value passes even though it fails outside the range.
Choose inputs that exercise each branch, including boundaries and errors.