Python Assert: Syntax, Examples, and When to Use It
Python assert checks a condition and raises AssertionError when it is false. Use it for developer assumptions, not input or security validation.
Python
assertis for internal conditions whose failure means the code is buggy; useraisefor validation or behavior that must always run.
What Does Assert Do in Python?
An assertion is an executable claim about the current state of a program. It says, “If the code is working as intended, this condition must be true.”
The simplest form contains the assert keyword followed by a condition:
assert condition
When assertions are enabled, Python tests the condition using its normal truth-value rules. If the condition is truthy, execution continues. If it is falsy, Python raises AssertionError. Under -O, the assertion is omitted.
This standalone example shows both paths:
temperature = 18
assert temperature > 0
print("The assertion passed")
try:
assert temperature < 0
except AssertionError:
print("The assertion failed")
The assertion passed
The assertion failed
The first assertion does nothing visible because temperature > 0 is true. The second condition is false, so it raises AssertionError. The try block catches that exception only so the example can display a stable result.
An assert statement is tied to the built-in __debug__ constant. Its behavior is equivalent to this nested structure:
if __debug__:
if not condition:
raise AssertionError
That outer if __debug__ matters. Python normally sets __debug__ to True, but it sets the constant to False when started with the -O option. In that mode, assertions are removed rather than evaluated.
This is why assert should express a developer assumption, not enforce a rule that the running application depends on. It belongs alongside the deeper language behavior covered in a structured Python learning roadmap, after basic conditions and exceptions are clear.
Python Assert Syntax and Worked Examples
Python has two assertion forms:
assert condition
assert condition, message
When assertions are enabled, the second form supplies an argument to AssertionError when the condition is falsy; under -O, the assertion is omitted. A useful message states what should have been true and includes the value that violated the assumption.
Here is a complete standalone example:
def average(numbers):
if not numbers:
raise ValueError("numbers must not be empty")
return sum(numbers) / len(numbers)
result = average([2, 4, 6])
assert result == 4
print(result)
try:
assert result == 5, f"expected 5, got {result}"
except AssertionError as error:
print(type(error).__name__)
print(error)
4.0
AssertionError
expected 5, got 4.0
The explicit check validates the collection state: numbers must contain at least one item before the division occurs. The assertion checks a function result. The failed assertion demonstrates the optional message.
If a failed assertion is not caught, Python prints a traceback. The traceback shows the chain of calls that reached the failing line, followed by a final line such as:
AssertionError: expected 5, got 4.0
Start at that final line to read the message, then inspect the preceding file and line information to locate the failed assumption. The assertion identifies the symptom. The actual defect may be earlier in the calculation.
Assertions use normal truth-value testing. None, False, numeric zero, empty strings, and empty collections are falsy. Non-empty collections are generally truthy, even when the item inside them is itself falsy.
values = [None, False, 0, "", [], {}, [0]]
print([bool(value) for value in values])
[False, False, False, False, False, False, True]
That makes concise checks possible, but the expression must still communicate the intended rule:
score = 82
assert 0 <= score <= 100, f"score outside expected range: {score}"
pending_jobs = ["resize-image"]
assert pending_jobs, "scheduler selected an empty job queue"
def square(number):
return number * number
answer = square(5)
assert answer == 25, f"unexpected square result: {answer}"
These examples cover three common shapes:
- A value must remain inside a valid internal range.
- A collection must have the expected state at a specific point.
- A function must produce a promised result.
Do not use a bare assertion merely because an expression can be tested for truth. assert result says only that result must be truthy. assert result.status == "ready" records a much more precise assumption.
When to Use Assert and When to Raise an Exception
The practical decision rule is based on who or what can cause the condition to fail.
An internal invariant is a condition that can fail only because the program has a defect. It describes something the code promises to keep true. Assertions are suitable for checking internal invariants because a failure means a developer must fix the code.
Examples include:
- A balance calculated from validated internal records cannot be negative.
- A sorting function’s result must contain the same number of items as its input.
- An internal state transition must produce one of the states handled by the next step.
- A function’s result must satisfy its documented postcondition.
A postcondition is an invariant checked after an operation. This standalone function uses assertions to verify properties of its own result:
def ascending_copy(numbers):
result = sorted(numbers)
assert len(result) == len(numbers), "sorting changed the item count"
assert all(
result[index] <= result[index + 1]
for index in range(len(result) - 1)
), f"result is not ascending: {result}"
return result
print(ascending_copy([5, 1, 3]))
[1, 3, 5]
Both conditions describe promises made by the implementation. If either fails, the sorting code or the assumptions around it are defective.
An apparently unreachable branch can also expose a broken invariant. Suppose an internal workflow is intended to produce only "queued" or "complete". Reaching a third state indicates a programming defect. An assertion can document that assumption during development:
def internal_label(state):
if state == "queued":
return "Waiting"
if state == "complete":
return "Done"
assert False, f"unhandled internal state: {state}"
There is an important qualification: with -O, that final assertion disappears and the function can return None. If continuing from the branch would corrupt state, disclose protected information, or otherwise break required behavior, use an explicit exception instead:
def internal_label(state):
if state == "queued":
return "Waiting"
if state == "complete":
return "Done"
raise RuntimeError(f"unhandled internal state: {state}")
Expected runtime failures belong to raise, even when they indicate that a value is unacceptable. User input, API responses, file availability, permissions, and authentication can all fail without proving that the program contains a defect.
This validation must remain active:
def set_discount(percent):
if not isinstance(percent, (int, float)):
raise TypeError("percent must be a number")
if not 0 <= percent <= 100:
raise ValueError("percent must be between 0 and 100")
return percent
Calling set_discount(140) supplies invalid input, so the function raises ValueError. The caller can correct the value or report the problem. Replacing either check with assert would allow invalid input through when assertions are removed.
Use the same reasoning for failures outside the program’s control:
| Situation | Use | Reason |
|---|---|---|
| Internal result violates a postcondition | assert | Failure indicates defective code |
| Internal data structure has an impossible state | assert, if optimized fallthrough is harmless | Failure identifies a broken developer assumption |
| Public function receives an invalid argument | raise | Callers can supply invalid values |
| Parsed external data has the wrong format | raise | External data is not controlled by the implementation |
| A required file is missing | Explicit exception handling | Missing resources are runtime failures |
| A user lacks permission | Explicit validation or raise | Permission checks must always run |
| Authentication fails | Explicit validation or raise | Security behavior cannot disappear |
| Continuing after an impossible branch would be unsafe | raise | The failure must occur in every execution mode |
This distinction also prevents a common misunderstanding: assertions are not a shorter spelling for validation. They have different execution semantics. The common Python mistakes guide covers similar cases where compact code silently changes the intended contract.
Using Assert in Pytest and Unittest
Assertions inside tests have a focused purpose: they compare observed behavior with expected behavior. The test fails when those values differ.
Pytest accepts ordinary Python assert statements. It rewrites assertions before executing the test so that failures can report values from common subexpressions.
Start with this application module:
# cart.py
def total(prices):
if any(price < 0 for price in prices):
raise ValueError("prices cannot be negative")
return sum(prices)
A pytest test can use a plain assertion:
# test_cart_pytest.py
from cart import total
def test_total_adds_prices():
assert total([3, 4, 5]) == 12
If the comparison fails, pytest can display useful details about the values involved. This richer test report does not change the purpose of assert in application code. The ValueError inside total remains necessary because rejecting negative prices is part of the function’s behavior.
The equivalent unittest test uses a named TestCase method:
# test_cart_unittest.py
import unittest
from cart import total
class TestTotal(unittest.TestCase):
def test_total_adds_prices(self):
self.assertEqual(total([3, 4, 5]), 12)
if __name__ == "__main__":
unittest.main()
assertEqual(actual, expected) communicates an equality check directly. Unittest also provides assertTrue for truth checks and assertRaises for expected exceptions.
For example, this unittest verifies the validation already implemented by total:
def test_total_rejects_negative_prices(self):
with self.assertRaises(ValueError):
total([3, -1, 5])
The matching pytest style uses its exception-checking helper:
import pytest
from cart import total
def test_total_rejects_negative_prices():
with pytest.raises(ValueError):
total([3, -1, 5])
The roles are separate:
| Mechanism | Typical location | Purpose |
|---|---|---|
Plain assert under pytest | Test code | Compare actual behavior with expected behavior |
TestCase.assertEqual | Unittest code | Check equality with a named assertion method |
TestCase.assertTrue | Unittest code | Check that an expression is truthy |
assertRaises or pytest.raises | Test code | Verify that expected code raises an exception |
Explicit raise | Application code | Enforce validation and required runtime behavior |
A test assertion verifies that application validation works. It does not perform that validation for the application.
Assertions, exception questions, and failure interpretation also appear in Python assessment tests and broader Python interview questions. The useful skill is explaining why a particular check belongs in a test, an internal assertion, or an explicit exception.
What Happens to Assert Under Python -O?
Python’s -O command-line option removes assert statements and code conditional on __debug__. Under ordinary execution, __debug__ is True. Under python -O, it is False.
The following harmless program makes the difference visible. The assertion condition calls a function that increments a counter, so the output proves whether Python evaluated the condition.
# optimized_assert.py
checks_run = 0
def check_inventory():
global checks_run
checks_run += 1
return False
try:
assert check_inventory(), "inventory mismatch"
except AssertionError as error:
print(type(error).__name__, str(error))
print("__debug__:", __debug__)
print("checks run:", checks_run)
Run it normally:
python optimized_assert.py
AssertionError inventory mismatch
__debug__: True
checks run: 1
The condition runs, returns False, and causes AssertionError. The exception message is evaluated and displayed.
Now run the same file with optimization enabled:
python -O optimized_assert.py
__debug__: False
checks run: 0
The assertion disappears. Python does not call check_inventory(), does not increment the counter, and does not evaluate the message.
This example establishes two rules.
First, never put a required side effect inside an assertion condition:
# Wrong: registration can disappear under python -O.
assert register_user(user)
Perform the action separately, then handle its result with ordinary control flow:
registered = register_user(user)
if not registered:
raise RuntimeError("user registration failed")
Second, do not put required work inside the assertion message. The optional message expression is needed only when the assertion fails, and the whole assertion can be removed:
# Wrong if record_failure() is required application behavior.
assert balance >= 0, record_failure(balance)
Authorization and authentication checks are especially clear cases. This code is unsafe because optimized execution can skip the check:
# Wrong: access control must not depend on assert.
assert user.is_admin, "administrator access required"
delete_record(record_id)
Use an explicit branch and exception:
if not user.is_admin:
raise PermissionError("administrator access required")
delete_record(record_id)
The same rule applies to input validation, state updates, billing decisions, data integrity checks, and any other behavior that must remain active. Assertions may describe internal assumptions around such code, but they cannot enforce its required contract.
Common Assert Mistakes
The most deceptive syntax mistake is writing assert(condition, message):
condition = False
message = "condition should be true"
assert (condition, message)
print("The tuple was truthy")
Standard output:
The tuple was truthy
This does not supply two parts to the assert statement. (condition, message) creates one non-empty tuple, and a non-empty tuple is truthy. Python emits a SyntaxWarning for this pattern, but the correct syntax is still worth memorizing:
assert condition, message
Other mistakes follow from treating assertions as ordinary error handling:
- Putting required side effects in the condition or message.
- Catching
AssertionErroras routine control flow. - Using
assertto reject user input or external data. - Protecting permissions, authentication, or sensitive operations with
assert. - Writing vague messages such as
"failed"instead of naming the broken assumption. - Packing several unrelated checks into one complicated Boolean expression.
- Assuming a pytest failure report makes
assertappropriate for production validation.
Use this checklist when choosing:
| Question | Choice |
|---|---|
| Can this fail only because the program is defective? | Consider assert |
| Can a user, caller, file, network response, or external service cause it? | Use validation and raise |
| Must the check run in every execution mode? | Use explicit control flow |
| Does the expression perform required work? | Move the work outside the assertion |
| Would continuing after failure be unsafe? | Raise an explicit exception |
| Is this a test comparison? | Use the testing framework’s normal assertion style |
Where the Books Fit
Assertions make more sense once conditions, exceptions, functions, testing, and application boundaries are studied together. Python in Three Months follows that job-ready path and gives each tool a defined role, so an internal debugging check does not accidentally become missing production validation. Keep the decision rule close: assert developer assumptions, and explicitly raise the failures the application must enforce.
Frequently asked questions
What does assert do in Python?
The assert statement checks a condition that should be true if the program is correct. When assertions are enabled, a falsy condition raises AssertionError, optionally with a message supplied after a comma; under -O, the assertion is omitted.
When should I use assert instead of raise?
Use assert for internal assumptions whose failure indicates a programming defect. Use raise for invalid arguments, external data, missing resources, permissions, authentication, and any behavior that must remain active in every execution mode.
Are Python assert statements removed with the -O option?
Yes. When Python starts with the -O option, assert statements and code conditional on __debug__ are removed, and __debug__ is false. Required validation, authorization, state changes, and other application behavior must therefore use explicit code.
Can I add a message to a Python assertion?
Yes. Write assert condition, message without wrapping both expressions in one pair of parentheses. If the assertion fails, Python passes the message to AssertionError.
Should pytest tests use plain assert statements?
Pytest accepts standard Python assert statements and can show values from common subexpressions when a test fails. Unittest instead provides named methods such as assertEqual, assertTrue, and assertRaises.