Python’s assert
statement allows you to write sanity checks in your code. These checks are known as assertions, and you can use them to test if certain assumptions remain true while you’re developing your code. If any of your assertions turn false, then you have a bug in your code.
Assertions are a convenient tool for documenting, debugging, and testing code during development. Once you’ve debugged and tested your code with the help of assertions, then you can turn them off to optimize the code for production. Assertions will help you make your code more efficient, robust, and reliable.
In this tutorial, you’ll learn:
- What assertions are and when to use them
- How Python’s
assert
statement works - How
assert
can help you document, debug, and test your code - How assertions can be disabled to improve performance in production
- What common pitfalls you might face when using
assert
statements
To get the most out of this tutorial, you should have previous knowledge of expressions and operators, functions, conditional statements, and exceptions. Having a basic understanding of documenting, debugging, and testing Python code is also a plus.
Getting to Know Assertions in Python
Python implements a feature called assertions that’s pretty useful during the development of your applications and projects. You’ll find this feature in several other languages too, such as C and Java, and it comes in handy for documenting, debugging, and testing your code.
If you’re looking for a tool to strengthen your debugging and testing process, then assertions are for you. In this section, you’ll learn the basics of assertions, including what they are, what they’re good for, and when you shouldn’t use them in your code.
What Are Assertions?
In Python, assertions are statements that you can use to set sanity checks during the development process. Assertions allow you to test the correctness of your code by checking if some specific conditions remain true, which can come in handy while you’re debugging code.
The assertion condition should always be true unless you have a bug in your program. If the condition turns out to be false, then the assertion raises an exception and terminates the execution of your program.
With assertions, you can set checks to make sure that invariants within your code stay invariant. By doing so, you can check assumptions like preconditions and postconditions. For example, you can test conditions along the lines of This argument is not None
or This return value is a string. These kinds of checks can help you catch errors as soon as possible when you’re developing a program.
What Are Assertions Good For?
Assertions are mainly for debugging. They’ll help you ensure that you don’t introduce new bugs while adding features and fixing other bugs in your code. However, they can have other interesting use cases within your development process. These use cases include documenting and testing your code.
The primary role of assertions is to trigger the alarms when a bug appears in a program. In this context, assertions mean Make sure that this condition remains true. Otherwise, throw an error.
In practice, you can use assertions to check preconditions and postconditions in your programs at development time. For example, programmers often place assertions at the beginning of functions to check if the input is valid (preconditions). Programmers also place assertions before functions’ return values to check if the output is valid (postconditions).
Assertions make it clear that you want to check if a given condition is and remains true. In Python, they can also include an optional message to unambiguously describe the error or problem at hand. That’s why they’re also an efficient tool for documenting code. In this context, their main advantage is their ability to take concrete action instead of being passive, as comments and docstrings are.
Finally, assertions are also ideal for writing test cases in your code. You can write concise and to-the-point test cases because assertions provide a quick way to check if a given condition is met or not, which defines if the test passes or not.
You’ll learn more about these common use cases of assertions later in this tutorial. Now you’ll learn the basics of when you shouldn’t use assertions.
When Not to Use Assertions?
In general, you shouldn’t use assertions for data processing or data validation, because you can disable assertions in your production code, which ends up removing all your assertion-based processing and validation code. Using assertions for data processing and validation is a common pitfall, as you’ll learn in Understanding Common Pitfalls of assert
later in this tutorial.
Additionally, assertions aren’t an error-handling tool. The ultimate purpose of assertions isn’t to handle errors in production but to notify you during development so that you can fix them. In this regard, you shouldn’t write code that catches assertion errors using a regular try
… except
statement.
Understanding Python’s assert
Statements
Now you know what assertions are, what they’re good for, and when you shouldn’t use them in your code. It’s time to learn the basics of writing your own assertions. First, note that Python implements assertions as a statement with the assert
keyword rather than as a function. This behavior can be a common source of confusion and issues, as you’ll learn later in this tutorial.
In this section, you’ll learn the basics of using the assert
statement to introduce assertions in your code. You’ll study the syntax of the assert
statement. Most importantly, you’ll understand how this statement works in Python. Finally, you’ll also learn the basics of the AssertionError
exception.
The Syntax of the assert
Statement
An assert
statement consists of the assert
keyword, the expression or condition to test, and an optional message. The condition is supposed to always be true. If the assertion condition is true, then nothing happens, and your program continues its normal execution. On the other hand, if the condition becomes false, then assert
halts the program by raising an AssertionError
.
In Python, assert
is a simple statement with the following syntax:
assert expression[, assertion_message]
Here, expression
can be any valid Python expression or object, which is then tested for truthiness. If expression
is false, then the statement throws an AssertionError
. The assertion_message
parameter is optional but encouraged. It can hold a string describing the issue that the statement is supposed to catch.
Here’s how this statement works in practice:
>>>
>>> number = 42
>>> assert number > 0
>>> number = -42
>>> assert number > 0
Traceback (most recent call last):
...
AssertionError
With a truthy expression, the assertion succeeds, and nothing happens. In that case, your program continues its normal execution. In contrast, a falsy expression makes the assertion fail, raising an AssertionError
and breaking the program’s execution.
To make your assert
statements clear to other developers, you should add a descriptive assertion message:
>>>
>>> number = 42
>>> assert number > 0, f"number greater than 0 expected, got: {number}"
>>> number = -42
>>> assert number > 0, f"number greater than 0 expected, got: {number}"
Traceback (most recent call last):
...
AssertionError: number greater than 0 expected, got: -42
The message in this assertion clearly states which condition should be true and what is making that condition fail. Note that the assertion_message
argument to assert
is optional. However, it can help you better understand the condition under test and figure out the problem that you’re facing.
So, whenever you use assert
, it’s a good idea to use a descriptive assertion message for the traceback of the AssertionError
exception.
An important point regarding the assert
syntax is that this statement doesn’t require a pair of parentheses to group the expression and the optional message. In Python, assert
is a statement instead of a function. Using a pair of parentheses can lead to unexpected behaviors.
For example, an assertion like the following will raise a SyntaxWarning
:
>>>
>>> number = 42
>>> assert(number > 0, f"number greater than 0 expected, got: {number}")
<stdin>:1: SyntaxWarning: assertion is always true, perhaps remove parentheses?
This warning has to do with non-empty tuples always being truthy in Python. In this example, the parentheses turn the assertion expression and message into a two-item tuple, which always evaluates to true.
Fortunately, recent versions of Python throw a SyntaxWarning
to alert you of this misleading syntax. However, in older versions of the language, an assert
statement like the one above will always succeed.
This issue often appears when you’re using long expressions or messages that take more than a single line. In these cases, the parentheses are the natural way to format the code, and you may end up with something like the following:
number = 42
assert (
number > 0 and isinstance(number, int),
f"number greater than 0 expected, got: {number}"
)
Using a pair of parentheses to split a long line into multiple lines is a common formatting practice in Python code. However, in the context of an assert
statement, the parentheses turn the assertion expression and message into a two-item tuple.
In practice, if you want to split a long assertion into several lines, then you can use the backslash character () for explicit line joining:
number = 42
assert number > 0 and isinstance(number, int),
f"number greater than 0 expected, got: {number}"
The backslash at the end of first line of this assertion joins the assertion’s two physical lines into a single logical line. This way, you can have appropriate line length without the risk of a warning or a logical error in your code.
There’s an edge case of this parentheses-related issue. If you provide only the assertion expression in parentheses, then assert
will work just fine:
>>>
>>> number = 42
>>> assert(number > 0)
>>> number = -42
>>> assert(number > 0)
Traceback (most recent call last):
...
AssertionError
Why is this happening? To create a single-item tuple, you need to place a comma after the item itself. In the code above, the parentheses by themselves don’t create a tuple. That’s why the interpreter ignores the parentheses, and assert
works as expected.
Even though the parentheses seem to work in the scenario described in the above example, it’s not a recommended practice. You can end up shooting yourself in the foot.
The AssertionError
Exception
If the condition of an assert
statement evaluates to false, then assert
raises an AssertionError
. If you provide the optional assertion message, then this message is internally used as an argument to the AssertionError
class. Either way, the raised exception breaks your program’s execution.
Most of the time, you won’t raise AssertionError
exceptions explicitly in your code. The assert
statement takes care of raising this exception when the assertion condition fails. Additionally, you shouldn’t attempt to handle errors by writing code that catches the AssertionError
exception, as you’ll learn later in this tutorial.
Finally, AssertionError
is a built-in exception that inherits from the Exception
class and is considered a concrete exception that should be raised instead of subclassed.
That’s it! Now you know the basics of the assert
statement. You’ve learned the statement’s syntax, how assert
works in practice, and also what the main characteristics of the AssertionError
exception are. It’s time to move forward and explore some effective and common ways to write assertions in Python.
Exploring Common Assertion Formats
When it comes to writing the assert
statement, you’ll find several assertion formats that are common in Python code. Being aware of these formats will allow you to write better assertions.
The following examples showcase a few of these common assertion formats, starting with assertions that compare objects:
>>>
>>> # Comparison assertions
>>> assert 3 > 2
>>> assert 3 == 2
Traceback (most recent call last):
...
AssertionError
>>> assert 3 > 2 and 5 < 10
>>> assert 3 == 2 or 5 > 10
Traceback (most recent call last):
...
AssertionError
Comparison assertions are intended to test conditions that compare two or more objects using comparison operators. These assertions can also include compound expressions based on Boolean operators.
Another common assertion format is related to membership tests:
>>>
>>> # Membership assertions
>>> numbers = [1, 2, 3, 4, 5]
>>> assert 4 in numbers
>>> assert 10 in numbers
Traceback (most recent call last):
...
AssertionError
Membership assertions allow you to check if a given item is present in a specific collection, such as a list, tuple, set, dictionary, or the like. These assertions use the membership operators, in
and not in
, to perform the required check.
The assertion format in the example below is related to an object’s identity:
>>>
>>> # Identity assertions
>>> x = 1
>>> y = x
>>> null = None
>>> assert x is y
>>> assert x is not y
Traceback (most recent call last):
...
AssertionError
>>> assert null is None
>>> assert null is not None
Traceback (most recent call last):
...
AssertionError
Identity assertions provide a way to test for an object’s identity. In this case, the assertion expression uses the identity operators, is
and is not
.
Finally, you’ll learn how to check the data type of your objects in the context of an assertion:
>>>
>>> # Type check assertions
>>> number = 42
>>> assert isinstance(number, int)
>>> number = 42.0
>>> assert isinstance(number, int)
Traceback (most recent call last):
...
AssertionError
Type check assertions commonly involve using the built-in isinstance()
function to make sure that a given object is an instance of a certain class or classes.
Even though these are some of the most common assertion formats that you’ll find in Python code, there are many other possibilities. For example, you can use the built-in all()
and any()
functions to write assertions that check for the truth value of items in an iterable:
>>>
>>> assert all([True, True, True])
>>> assert all([True, False, True])
Traceback (most recent call last):
...
AssertionError
>>> assert any([False, True, False])
>>> assert any([False, False, False])
Traceback (most recent call last):
...
AssertionError
The all()
assertions check if all the items in an input iterable are truthy, while the any()
examples check if any item in the input iterable is truthy.
Your imagination is the only limit for writing useful assertions. You can write assertions using predicate or Boolean-valued functions, regular Python objects, comparison expressions, Boolean expressions, or general Python expressions. Your assertion will depend on what specific condition you need to check at a given moment.
Now you know some of the most common assertion formats that you can use in your code. It’s time to learn about specific use cases of assertions. In the following section, you’ll learn how to use assertions to document, debug, and test your code.
Documenting Your Code With Assertions
The assert
statement is an effective way to document code. For example, if you want to state that a specific condition
should always be true in your code, then assert condition
can be better and more effective than a comment or a docstring, as you’ll learn in a moment.
To understand why assertions can be a handy documenting tool, say that you have a function that takes a server name and a tuple of port numbers. The function will iterate over the port numbers trying to connect to the target server. For your function to work correctly, the tuple of ports shouldn’t be empty:
def get_response(server, ports=(443, 80)):
# The ports argument expects a non-empty tuple
for port in ports:
if server.connect(port):
return server.get()
return None
If someone accidentally calls get_response()
with an empty tuple, then the for
loop will never run, and the function will return None
even if the server is available. To alert programmers to this buggy call, you can use a comment, like you did in the example above. However, using an assert
statement can be more effective:
def get_response(server, ports=(443, 80)):
assert len(ports) > 0, f"ports expected a non-empty tuple, got {ports}"
for port in ports:
if server.connect(port):
return server.get()
return None
The advantage of an assert
statement over a comment is that when the condition isn’t true, assert
immediately raises an AssertionError
. After that, your code stops running, so it avoids abnormal behaviors and points you directly to the specific problem.
So, using assertions in situations like the one described above is an effective and powerful way to document your intentions and avoid hard-to-find bugs due to accidental errors or malicious actors.
Debugging Your Code With Assertions
At its core, the assert
statement is a debugging aid for testing conditions that should remain true during your code’s normal execution. For assertions to work as a debugging tool, you should write them so that a failure indicates a bug in your code.
In this section, you’ll learn how to use the assert
statement to assist you while debugging your code at development time.
An Example of Debugging With Assertions
You’ll typically use assertions to debug your code during development. The idea is to make sure that specific conditions are and remain true. If an asserted condition becomes false, then you immediately know that you have a bug.
As an example, say that you have the following Circle
class:
# circle.py
import math
class Circle:
def __init__(self, radius):
if radius < 0:
raise ValueError("positive radius expected")
self.radius = radius
def area(self):
assert self.radius >= 0, "positive radius expected"
return math.pi * self.radius ** 2
The class’s initializer, .__init__()
, takes radius
as an argument and makes sure that the input value is a positive number. This check prevents circles with a negative radius.
The .area()
method computes the circle’s area. However, before doing that, the method uses an assert
statement to guarantee that .radius
remains a positive number. Why would you add this check? Well, suppose that you’re working on a team, and one of your coworkers needs to add the following method to Circle
:
class Circle:
# ...
def correct_radius(self, correction_coefficient):
self.radius *= correction_coefficient
This method takes a correction coefficient and applies it to the current value of .radius
. However, the method doesn’t validate the coefficient, introducing a subtle bug. Can you spot it? Say that the user provides a negative correction coefficient by accident:
>>>
>>> from circle import Circle
>>> tire = Circle(42)
>>> tire.area()
5541.769440932395
>>> tire.correct_radius(-1.02)
>>> tire.radius
-42.84
>>> tire.area()
Traceback (most recent call last):
...
AssertionError: positive radius expected
The first call to .area()
works correctly because the initial radius is positive. But the second call to .area()
breaks your code with an AssertionError
. Why? This happens because the call to .correct_radius()
turns the radius into a negative number, which uncovers a bug: the function doesn’t properly check for valid input.
In this example, your assert
statement works as a watchdog for situations in which the radius could take invalid values. The AssertionError
immediately points you to the specific problem: .radius
has unexpectedly changed to a negative number. You have to figure out how this unexpected change happened and then fix your code before it goes into production.
A Few Considerations on Debugging With Assertions
Developers often use assert
statements to state preconditions, just like you did in the above example, where .area()
checks for a valid .radius
right before doing any computation. Developers also use assertions to state postconditions. For example, you can check if a function’s return value is valid, right before returning the value to the caller.
In general, the conditions that you check with an assert
statement should be true, unless you or another developer in your team introduces a bug in the code. In other words, these conditions should never be false. Their purpose is to quickly flag if someone introduces a bug. In this regard, assertions are early alerts in your code. These alerts are meant to be useful during development.
If one of these conditions fails, then the program will crash with an AssertionError
, telling you exactly which condition isn’t succeeding. This behavior will help you track down and fix bugs more quickly.
To properly use assertions as a debugging tool, you shouldn’t use try
… except
blocks that catch and handle AssertionError
exceptions. If an assertion fails, then your program should crash because a condition that was supposed to be true became false. You shouldn’t change this intended behavior by catching the exception with a try
… except
block.
A proper use of assertions is to inform developers about unrecoverable errors in a program. Assertions shouldn’t signal an expected error, like a FileNotFoundError
, where a user can take a corrective action and try again.
The goal of assertion should be to uncover programmers’ errors rather than users’ errors. Assertions are useful during the development process, not during production. By the time you release your code, it should be (mostly) free of bugs and shouldn’t require the assertions to work correctly.
Finally, once your code is ready for production, you don’t have to explicitly remove assertions. You can just disable them, as you’ll learn in the following section.
Disabling Assertions in Production for Performance
Now say that you’ve come to the end of your development cycle. Your code has been extensively reviewed and tested. All your assertions pass, and your code is ready for a new release. At this point, you can optimize the code for production by disabling the assertions that you added during development. Why should you optimize your code this way?
Assertions are great during development, but in production, they can affect the code’s performance. For example, a codebase with many assertions running all the time can be slower than the same code without assertions. Assertions take time to run, and they consume memory, so it’s advisable to disable them in production.
Now, how can you actually disable your assertions? Well, you have two options:
- Run Python with the
-O
or-OO
options. - Set the
PYTHONOPTIMIZE
environment variable to an appropriate value.
In this section, you’ll learn how to disable your assertions by using these two techniques. Before doing this, you’ll get to know the built-in __debug__
constant, which is the internal mechanism that Python uses to disable assertions.
Understanding the __debug__
Built-in Constant
Python has a built-in constant called __debug__
. This constant is closely related to the assert
statement. Python’s __debug__
is a Boolean constant, which defaults to True
. It’s a constant because you can’t change its value once your Python interpreter is running:
>>>
>>> import builtins
>>> "__debug__" in dir(builtins)
True
>>> __debug__
True
>>> __debug__ = False
File "<stdin>", line 1
SyntaxError: cannot assign to __debug__
In this code snippet, you first confirm that __debug__
is a Python built-in that’s always available for you. True
is the default value of __debug__
, and there’s no way to change this value once your Python interpreter is running.
The value of __debug__
depends on which mode Python runs in, normal or optimized:
Mode | Value of __debug__ |
---|---|
Normal (or debug) | True |
Optimized | False |
Normal mode is typically the mode that you use during development, while optimized mode is what you should use in production. Now, what does __debug__
have to do with assertions? In Python, the assert
statement is equivalent to the following conditional:
if __debug__:
if not expression:
raise AssertionError(assertion_message)
# Equivalent to
assert expression, assertion_message
If __debug__
is true, then the code under the outer if
statement runs. The inner if
statement checks expression
for truthiness and raises an AssertionError
only if the expression is not true. This is the default or normal Python mode, in which all your assertions are enabled because __debug__
is True
.
On the other hand, if __debug__
is False
, then the code under the outer if
statement doesn’t run, meaning that your assertions will be disabled. In this case, Python is running in optimized mode.
Normal or debug mode allows you to have assertions in place as you develop and test the code. Once your current development cycle is complete, then you can switch to optimized mode and disable the assertions to get your code ready for production.
To activate optimized mode and disable your assertions, you can either start up the Python interpreter with the –O
or -OO
option, or set the system variable PYTHONOPTIMIZE
to an appropriate value. You’ll learn how to do both operations in the following sections.
Running Python With the -O
or -OO
Options
You can disable all your assert
statements by having the __debug__
constant set to False
. To accomplish this task, you can use Python’s -O
or -OO
command-line options to run the interpreter in optimized mode.
The -O
option internally sets __debug__
to False
. This change removes the assert
statements and any code that you’ve explicitly introduced under a conditional targeting __debug__
. The -OO
option does the same as -O
and also discards docstrings.
Running Python with the -O
or -OO
command-line option makes your compiled bytecode smaller. Additionally, if you have several assertions or if __debug__:
conditionals, then these command-line options can also make your code faster.
Now, what effect does this optimization have on your assertions? It disables them. For an example, open your command line or terminal within the directory containing the circle.py
file and run an interactive session with the python -O
command. Once there, run the following code:
>>>
>>> # Running Python in optimized mode
>>> __debug__
False
>>> from circle import Circle
>>> # Normal use of Circle
>>> ring = Circle(42)
>>> ring.correct_radius(1.02)
>>> ring.radius
42.84
>>> ring.area()
5765.656926346065
>>> # Invalid use of Circle
>>> ring = Circle(42)
>>> ring.correct_radius(-1.02)
>>> ring.radius
-42.84
>>> ring.area()
5765.656926346065
Because the -O
option disables your assertions by setting __debug__
to False
, your Circle
class now accepts a negative radius, as the final example showcases. This behavior is completely wrong because you can’t have a circle with a negative radius. Additionaly, the circle’s area is computed using the wrong radius as an input.
The potential to disable assertions in optimized mode is the main reason why you must not use assert
statements to validate input data but as an aid to your debugging and testing process.
A Pythonic solution for the Circle
class would be to turn the .radius
attribute into a managed attribute using the @property
decorator. This way, you perform the .radius
validation every time the attribute changes:
# circle.py
import math
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("positive radius expected")
self._radius = value
def area(self):
return math.pi * self.radius ** 2
def correct_radius(self, correction_coefficient):
self.radius *= correction_coefficient
Now .radius
is a managed attribute that provides setter and getter methods using the @property
decorator. You’ve moved the validation code from .__init__()
to the setter method, which is called whenever the class changes the value of .radius
.
Now, the updated Circle
works as expected if you run the code in optimized mode:
>>>
>>> # Running Python in optimized mode
>>> __debug__
False
>>> from circle import Circle
>>> # Normal use of Circle
>>> ring = Circle(42)
>>> ring.correct_radius(1.02)
>>> ring.radius
42.84
>>> ring.area()
5765.656926346065
>>> # Invalid use of Circle
>>> ring = Circle(42)
>>> ring.correct_radius(-1.02)
Traceback (most recent call last):
...
ValueError: positive radius expected
Circle
always validates the value of .radius
before assignment, and your class works correctly, raising a ValueError
for negative values of .radius
. That’s it! You’ve fixed the bug with an elegant solution.
An interesting side effect of running Python in optimized mode is that code under an explicit if __debug__:
condition is also disabled. Consider the following script:
# demo.py
print(f"{__debug__ = }")
if __debug__:
print("Running in Normal mode!")
else:
print("Running in Optimized mode!")
This script explicitly checks the value of __debug__
in an if
… else
statement. The code in the if
code block will run only if __debug__
is True
. In contrast, if __debug__
is False
, then the code in the else
block will run.
Now try running the script in normal and optimized mode to check its behavior in each mode:
$ python demo.py
__debug__ = True
Running in Normal mode!
$ python -O demo.py
__debug__ = False
Running in Optimized mode!
When you execute the script in normal mode, the code under the if __debug__:
condition runs because __debug__
is True
in this mode. On the other hand, when you execute the script in optimized mode with the -O
option, __debug__
changes to False
, and the code under the else
block runs.
Python’s -O
command-line option removes assertions from the resulting compiled bytecode. Python’s -OO
option performs the same kind of optimization as -O
, with the addition of removing docstrings from your bytecode.
Because both options set __debug__
to False
, any code under an explicit if __debug__:
conditional also stops working. This behavior provides a powerful mechanism to introduce debugging-only code in your Python projects during their development stages.
Now you know the basics of using Python’s -O
and -OO
options to disable your assertions in production code. However, running Python with either of these options every time you need to run your production code seems repetitive and may be error-prone. To automate the process, you can use the PYTHONOPTIMIZE
environment variable.
Setting the PYTHONOPTIMIZE
Environment Variable
You can also run Python in optimized mode with disabled assertions by setting the PYTHONOPTIMIZE
environment variable to an appropriate value. For example, setting this variable to a non-empty string is equivalent to running Python with the -O
option.
To try PYTHONOPTIMIZE
out, fire up your command line and run the following command:
- Windows
- Linux + macOS
C:> set PYTHONOPTIMIZE="1"
$ export PYTHONOPTIMIZE="1"
Once you’ve set PYTHONOPTIMIZE
to a non-empty string, you can launch your Python interpreter with the bare-bones python
command. This command will automatically run Python in optimized mode.
Now go ahead and run the following code from the directory containing your circle.py
file:
>>>
>>> from circle import Circle
>>> # Normal use of Circle
>>> ring = Circle(42)
>>> ring.correct_radius(1.02)
>>> ring.radius
42.84
>>> # Invalid use of Circle
>>> ring = Circle(42)
>>> ring.correct_radius(-1.02)
>>> ring.radius
-42.84
Again, your assertions are off, and the Circle
class accepts negative radius values. You’re running Python in optimized mode again.
Another possibility is to set PYTHONOPTIMIZE
to an integer value, n
, which is equivalent to running Python using the -O
option n
times. In other words, you’re using n
levels of optimization:
- Windows
- Linux + macOS
C:> set PYTHONOPTIMIZE=1 # Equivalent to python -O
C:> set PYTHONOPTIMIZE=2 # Equivalent to python -OO
$ export PYTHONOPTIMIZE=1 # Equivalent to python -O
$ export PYTHONOPTIMIZE=2 # Equivalent to python -OO
You can use any integer number to set PYTHONOPTIMIZE
. However, Python only implements two levels of optimization. Using a number greater than 2
has no real effect on your compiled bytecode. Additionally, setting PYTHONOPTIMIZE
to 0
will cause the interpreter to run in normal mode.
Running Python in Optimized Mode
When you run Python, the interpreter compiles any imported module to bytecode on the fly. The compiled bytecode will live in a directory called __pycache__/
, which is placed in the directory containing the module that provided the imported code.
Inside __pycache__/
, you’ll find a .pyc
file named after your original module plus the interpreter’s name and version. The name of the .pyc
file will also include the optimization level used to compile the code.
For example, when you import code from circle.py
, the Python 3.10 interpreter generates the following files, depending on the optimization level:
File Name | Command | PYTHONOPTIMIZE |
---|---|---|
circle.cpython-310.pyc |
python circle.py |
0 |
circle.cpython-310.opt-1.pyc |
python -O circle.py |
1 |
circle.cpython-310.opt-2.pyc |
python -OO circle.py |
2 |
The name of each file in this table includes the original module’s name (circle
), the interpreter that generated the code (cpython-310
), and the optimization level (opt-x
). The table also summarizes the corresponding commands and values for the PYTHONOPTIMIZE
variable. PEP 488 provides more context on this naming format for .pyc
files.
The main results of running Python in the first level of optimization is that the interpreter sets __debug__
to False
and removes the assertions from the resulting compiled bytecode. These optimizations make the code smaller and potentially faster than the same code running in normal mode.
The second level of optimization does the same as the first level. It also removes all the docstrings from the compiled code, which results in an even smaller compiled bytecode.
Testing Your Code With Assertions
Testing is another field in the development process where assertions are useful. Testing boils down to comparing an observed value with an expected one to check if they’re equal or not. This kind of check perfectly fits into assertions.
Assertions must check for conditions that should typically be true, unless you have a bug in your code. This idea is another important concept behind testing.
The pytest
third-party library is a popular testing framework in Python. At its core, you’ll find the assert
statement, which you can use to write most of your test cases in pytest
.
Here are a few examples of writing test cases using assert
statements. The examples below take advantage of some built-in functions, which provide the testing material:
# test_samples.py
def test_sum():
assert sum([1, 2, 3]) == 6
def test_len():
assert len([1, 2, 3]) > 0
def test_reversed():
assert list(reversed([1, 2, 3])) == [3, 2, 1]
def test_membership():
assert 3 in [1, 2, 3]
def test_isinstance():
assert isinstance([1, 2, 3], list)
def test_all():
assert all([True, True, True])
def test_any():
assert any([False, True, False])
def test_always_fail():
assert pow(10, 2) == 42
All these test cases use the assert
statement. Most of them are written using the assertion formats that you learned before. They all showcase how you’d write real-world test cases to check different pieces of your code with pytest
.
Now, why does pytest
favor plain assert
statements in test cases over a custom API, which is what other testing frameworks prefer? There are a couple of remarkable advantages behind this choice:
- The
assert
statement allowspytest
to lower the entry barrier and somewhat flatten the learning curve because its users can take advantage of Python syntax that they already know. - The users of
pytest
don’t need to import anything from the library to start writing test cases. They only need to start importing things if their test cases get complicated, demanding more advanced features.
These advantages make working with pytest
a pleasant experience for beginners and people coming from other testing frameworks with custom APIs.
For example, the standard-library unittest
module provides an API consisting of a list of .assert*()
methods that work pretty much like assert
statements. This kind of API can be difficult to learn and memorize for developers starting with the framework.
You can use pytest
to run all the test case examples above. First, you need to install the library by issuing the python -m pip install pytest
command. Then you can execute pytest test_samples.py
from the command-line. This latter command will display an output similar to the following:
========================== test session starts =========================
platform linux -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /home/user/python-assert
collected 8 items
test_samples.py .......F [100%]
========================== FAILURES =====================================
__________________________ test_always_fail _____________________________
def test_always_fail():
> assert pow(10, 2) == 42
E assert 100 == 42
E + where 100 = pow(10, 2)
test_samples.py:25: AssertionError
========================== short test summary info ======================
FAILED test_samples.py::test_always_fail - assert 100 == 42
========================== 1 failed, 7 passed in 0.21s ==================
The first highlighted line in this output tells you that pytest
discovered and ran eight test cases. The second highlighted line shows that seven out of eight tests passed successfully. That’s why you get seven green dots and an F
.
A remarkable feature to note is that pytest
integrates nicely with the assert
statement. The library can display error reports with detailed information about the failing assertions and why they’re failing. As an example, check out the the lines starting with the E
letter in the above output. They display error messages.
Those lines clearly uncover the root cause of the failure. In this example, pow(10, 2)
returns 100
instead of 42
, which is intentionally wrong. You can use pytest.raises()
to handle code that is expected to fail.
Understanding Common Pitfalls of assert
Even though assertions are such a great and useful tool, they have some downsides. Like any other tool, assertions can be misused. You’ve learned that you should use assertions mainly for debugging and testing code during development. In contrast, you shouldn’t rely on assertions to provide functionality in production code, which is one of the main drivers of pitfalls with assertions.
In particular, you may run into pitfalls if you use assertions for:
- Processing and validating data
- Handling errors
- Running operations with side effects
Another common source of issues with assertions is that keeping them enabled in production can negatively impact your code’s performance.
Finally, Python has assertions enabled by default, which can confuse developers coming from other languages. In the following sections, you’ll learn about all these possible pitfalls of assertions. You’ll also learn how to avoid them in your own Python code.
Using assert
for Data Processing and Validation
You shouldn’t use assert
statements to verify the user’s input or any other input data from external sources. That’s because production code typically disables assertions, which will remove all the verification.
For example, suppose you’re building an online store with Python, and you need to add functionality to accept discount coupons. You end up writing the following function:
# store.py
# Code under development
def price_with_discount(product, discount):
assert 0 < discount < 1, "discount expects a value between 0 and 1"
new_price = int(product["price"] * (1 - discount))
return new_price
Notice the assert
statement in the first line of price_with_discount()
? It’s there to guarantee that the discounted price won’t be equal to or lower than zero dollars. The assertion also ensures that the new price won’t be higher than the product’s original price.
Now consider the example of a pair of shoes at twenty-five percent off:
>>>
>>> from store import price_with_discount
>>> shoes = {"name": "Fancy Shoes", "price": 14900}
>>> # 25% off -> $111.75
>>> price_with_discount(shoes, 0.25)
11175
All right, price_with_discount()
works nicely! It takes the product as a dictionary, applies the intended discount to the current price, and returns the new price. Now, try to apply some invalid discounts:
>>>
>>> # 200% off
>>> price_with_discount(shoes, 2.0)
Traceback (most recent call last):
...
AssertionError: discount expects a value between 0 and 1
>>> # 100% off
>>> price_with_discount(shoes, 1)
Traceback (most recent call last):
...
AssertionError: discount expects a value between 0 and 1
Applying an invalid discount raises an AssertionError
that points out the violated condition. If you ever encounter this error while developing and testing your online store, then it shouldn’t be hard to figure out what happened by looking at the traceback.
The real problem with the example above comes if the end user can make direct calls to price_with_discount()
in production code with disabled assertions. In this situation, the function won’t check the input value for discount
, possibly accepting wrong values and breaking the correctness of your discount functionality.
In general, you can write assert
statements to process, validate, or verify data during development. However, if those operations remain valid in production code, then make sure to replace them with an if
statement or a try
… except
block.
Here’s a new version of price_with_discount()
that uses a conditional instead of an assertion:
# store.py
# Code in production
def price_with_discount(product, discount):
if 0 < discount < 1:
new_price = int(product["price"] * (1 - discount))
return new_price
raise ValueError("discount expects a value between 0 and 1")
In this new implementation of price_with_discount()
, you replace the assert
statement with an explicit conditional statement. The function now applies the discount only if the input value is between 0
and 1
. Otherwise, it raises a ValueError
, signaling the problem.
Now you can wrap up any calls to this function in a try
… except
block that catches the ValueError
and sends an informative message to the users so that they can take action accordingly.
The moral of this example is that you shouldn’t rely on the assert
statement for data processing or data validation, because this statement is typically turned off in production code.
Handling Errors With assert
Another important pitfall with assertions is that sometimes developers use them as a quick form of error handling. As a result, if the production code removes assertions, then important error checks are also removed from the code. So, keep in mind that assertions aren’t a replacement for good error handling.
Here’s an example of using assertions for error handling:
# Bad practice
def square(x):
assert x >= 0, "only positive numbers are allowed"
return x ** 2
try:
square(-2)
except AssertionError as error:
print(error)
If you execute this code in production with disabled assertions, then square()
will never run the assert
statement and raise an AssertionError
. In this situation, the try
… except
block is superfluous and nonfunctional.
What can you do to fix this example? Try updating square()
to use an if
statement and a ValueError
:
# Best practice
def square(x):
if x < 0:
raise ValueError("only positive numbers are allowed")
return x ** 2
try:
square(-2)
except ValueError as error:
print(error)
Now square()
deals with the condition by using an explicit if
statement that can’t be disabled in production code. Your try
… except
block now handles a ValueError
, which is a more appropriate exception in this example.
Don’t ever catch AssertionError
exceptions in your code, because that would silence failing assertions, which is a clear sign of misused assertions. Instead, catch concrete exceptions that are clearly related to the errors that you’re handling and let your assertions fail.
Use assertions only to check errors that shouldn’t happen during the normal execution of your programs unless you have a bug. Remember that assertions can be disabled.
Running assert
on Expressions With Side Effects
Another subtle pitfall with the assert
statement appears when you use this statement to check operations, functions, or expressions that have some kind of side effect. In other words, these operations modify the state of objects outside the operation’s scope.
In those situations, the side effect takes place every time your code runs the assertion, which might silently change your program’s global state and behavior.
Consider the following toy example, in which a function modifies the value of a global variable as a side effect:
>>>
>>> sample = [42, 27, 40, 38]
>>> def popped(sample, index=-1):
... item = sample.pop(index)
... return item
...
>>> assert sample[-1] == popped(sample)
>>> assert sample[1] == popped(sample, 1)
>>> sample
[42, 40]
In this example, popped()
returns item
at a given index
in the input sample
of data, with the side effect of also removing said item
.
Using assertions to make sure that your function is returning the correct item can seem appropriate. However, this will cause the function’s internal side effect to run in every assertion, modifying the original content of sample
.
To prevent unexpected behaviors like the one in the above example, use assertion expressions that don’t cause side effects. For example, you can use pure functions that just take input arguments and return the corresponding output without modifying the state of objects from other scopes and namespaces.
Impacting Performance With assert
Too many assertions in production can impact your code’s performance. This issue becomes critical when the asserted conditions involve too much logic, such as long compound conditions, long-running predicate functions, and classes implying a costly instantiation process.
Assertions can impact your code’s performance in two main ways. They will:
- Take time to execute
- Use extra memory
An assert
statement that checks for a None
value can be relatively inexpensive. However, more complex assertions, especially those running heavy code, can measurably slow down your code. Assertions also consume memory to store their own code and any required data.
To avoid performance issues in production code, you should use Python’s -O
or -OO
command-line options or set the PYTHONOPTIMIZE
environment variable according to your needs. Either strategy will optimize your code by generating an assertions-free compiled bytecode, which will run more quickly and take up less memory.
Additionally, to prevent performance issues during development, your assertions should be fairly slim and to the point.
Having assert
Statements Enabled by Default
In Python, assertions are enabled by default. When the interpreter runs in normal mode, the __debug__
variable is True
, and your assertions are enabled. This behavior makes sense because you typically develop, debug, and test your code in normal mode.
If you want to disable your assertions, then you need to do it explicitly. You can either run the Python interpreter with the -o
or -OO
options, or set the PYTHONOPTIMIZE
environment variable to a proper value.
In contrast, other programming languages have assertions disabled by default. For example, if you’re coming into Python from Java, you may assume that your assertions won’t run unless you explicitly turn them on. This assumption can be a common source of confusion for Python beginners, so keep it in mind.
Conclusion
Now you know how to use Python’s assert
statement to set sanity checks throughout your code and make sure that certain conditions are and remain true. When any of these conditions fail, you have a clear indication of what’s happening. This way, you can quickly debug and fix your code.
The assert
statement is pretty handy when you need to document, debug, and test your code during the development stages. In this tutorial, you learned how to use assertions in your code and how they can make your debugging and testing process more efficient and straightforward.
In this tutorial, you learned:
- What assertions are and when to use them
- How Python’s
assert
statement works - How
assert
is handy for documenting, debugging, and testing code - How assertions can be disabled to improve performance in production
- What common pitfalls you can face when using
assert
statements
With this knowledge on the assert
statement, you can now write robust, reliable, and less buggy code, which can take you to the next level as a developer.
Assertions in any programming language are the debugging tools that help in the smooth flow of code. Assertions are mainly assumptions that a programmer knows or always wants to be true and hence puts them in code so that failure of these doesn’t allow the code to execute further.
assert in Python
In simpler terms, we can say that assertion is the boolean expression that checks if the statement is True or False. If the statement is true then it does nothing and continues the execution, but if the statement is False then it stops the execution of the program and throws an error.
Flowchart of Python Assert Statement
assert in Python
assert Keyword in Python
In python, assert keyword helps in achieving this task. This statement takes as input a boolean condition, which when returns true doesn’t do anything and continues the normal flow of execution, but if it is computed to be false, then it raises an AssertionError along with the optional message provided.
Syntax : assert condition, error_message(optional)
Parameters :
condition : The boolean condition returning true or false.
error_message : The optional argument to be printed in console in case of AssertionErrorReturns :
Returns AssertionError, in case the condition evaluates to false along with the error message which when provided.
Example 1: Python assert keyword without error message
Python3
a
=
4
b
=
0
print
(
"The value of a / b is : "
)
assert
b !
=
0
print
(a
/
b)
Output :
The value of a / b is : --------------------------------------------------------------------------- AssertionError Traceback (most recent call last) Input In [19], in <cell line: 10>() 8 # using assert to check for 0 9 print("The value of a / b is : ") ---> 10 assert b != 0 11 print(a / b) AssertionError:
Example 2: Python assert keyword with error message
Python3
a
=
4
b
=
0
print
(
"The value of a / b is : "
)
assert
b !
=
0
,
"Zero Division Error"
print
(a
/
b)
Output:
AssertionError: Zero Division Error
Practical Application
This has a much greater utility in Testing and Quality Assurance role in any development domain. Different types of assertions are used depending upon the application. Below is the simpler demonstration of a program that only allows only the batch with all hot food to be dispatched, else rejects the whole batch.
Python3
batch
=
[
40
,
26
,
39
,
30
,
25
,
21
]
cut
=
26
for
i
in
batch:
assert
i >
=
26
,
"Batch is Rejected"
print
(
str
(i)
+
" is O.K"
)
Output :
40 is O.K 26 is O.K 39 is O.K 30 is O.K
Runtime Exception :
AssertionError: Batch is Rejected
Предыдущий урок: Потоки и многопоточность
Инструкции assert в Python — это булевы выражения, которые проверяют, является ли условие истинным (True
). Они определяют факты (утверждения) в программе. Assertion — это проверка, которую можно включить, а затем выключить, завершив тестирование программы.
Возьмем простой пример функции деления. Можно быть уверенным в том, что делитель не должен быть нолем. Это и указывается при тестировании. Разберем этот пример позже.
Что такое Assertion (утверждение)
Assertions (утверждения) — это инструкции, которые «утверждают» определенный кейс в программе. В Python они выступают булевыми выражениями, которые проверяют, является ли условие истинным или ложным. Если оно истинно, то программа ничего не делает и переходит к выполнению следующей строчки кода.
Но если оно ложно, то программа останавливается и возвращает ошибку.
Следующий синтаксис — это базовая структура инструкций утверждения в Python.
assert condition
Если же нужно добавить сообщение для вывода при ложном условии, то синтаксис будет таким.
assert condition, message
Это сообщение позволит лучше понять, почему код не сработал.
Если нужно симулировать или выполнить отладку кода, чтобы узнать, что именно происходит на определенном этапе, то «утверждения» в Python отлично для этого подходят.
Именно инструмент отладки останавливает программу, как только возникает какая-то ошибка. Он также показывает, где именно она произошла.
- Инструкция assert принимает выражение и необязательное сообщение;
- Она используется для проверки типов, значений аргумента и вывода функции;
- А также для отладки, поскольку приостанавливает программу в случае ошибки.
Вот пример работы утверждений в Python.
def avg(ranks):
assert len(ranks) != 0
return round(sum(ranks)/len(ranks), 2)
ranks = [62, 65, 75]
print("Среднее значение:", avg(ranks))
В этом примере нужно, чтобы пользователь не оставлял параметры пустыми. Если этого не сделать, вернется ошибка Assertion Error
. Вот пример вывода:
Среднее значение: 67.33
В этом случае параметры были переданы, поэтому функция вернула нужный результат.
Теперь попробуем ничего не передавать.
def avg(ranks):
assert len(ranks) != 0
return round(sum(ranks)/len(ranks), 2)
ranks = []
print("Среднее значение:", avg(ranks))
Длина массива ranks
оказалась 0, и python вернул ошибку Assertion Error
.
Traceback (most recent call last):
File "C:/Users/asd/AppData/Local/Programs/Python/Python38/wb.py", line 6, in
print("Среднее значение:", avg(ranks))
File "C:/Users/asd/AppData/Local/Programs/Python/Python38/wb.py", line 2, in avg
assert len(ranks) != 0
AssertionError
Исключения Assertion Error
можно перехватывать и обрабатывать как и любые другие исключения с помощью try-except. Но если их обработать неправильно, то программа остановится и вернет traceback
.
Однако в примере выше она не возвращает ошибку с нужным сообщением. Ее можно написать самостоятельно. Вот как это делается.
def avg(ranks):
assert len(ranks) != 0, 'Список ranks не должен быть пустым'
return round(sum(ranks)/len(ranks), 2)
ranks = []
print("Среднее значение:", avg(ranks))
Вторым аргументом к assert
в примере выше было передано сообщение, которое позже появится в выводе.
Traceback (most recent call last):
File "C:/Users/asd/AppData/Local/Programs/Python/Python38/wb.py", line 6, in
print("Среднее значение:", avg(ranks))
File "C:/Users/asd/AppData/Local/Programs/Python/Python38/wb.py", line 2, in avg
assert len(ranks) != 0, 'Список ranks не должен быть пустым'
AssertionError: Список ranks не должен быть пустым
Assert с сообщением об ошибки
Рассмотрим еще один пример с делением на 0.
def divide(x, y):
assert y != 0 , 'Нельзя делить на 0'
return round(x/y, 2)
z = divide(21,3)
print(z)
a = divide(21,0)
print(a)
В этом примере, если делителем будет ноль, то assert
вернет сообщение с ошибкой. Посмотрим на вывод.
7.0
Traceback (most recent call last):
File "C:/Users/asd/AppData/Local/Programs/Python/Python38/wb.py", line 8, in
a = divide(21,0)
File "C:/Users/asd/AppData/Local/Programs/Python/Python38/wb.py", line 2, in divide
assert y != 0 , 'Нельзя делить на 0'
AssertionError: Нельзя делить на 0
На третьей сверху строчке написана сама инструкция assert
. Именно здесь проверяется, не является ли переменная y
равной 0. Если она больше 0, то условие истинно, и код возвращает требуемый результат.
Но если вызвать метод division()
со вторым аргументом 0, то условие assert
будет ложным.
По этой причине и возникает исключение Assertion Error
. Именно оно возвращает ошибку с сообщением «Нельзя делить на 0».
Методы assert
Метод | Проверка на | Работает с |
---|---|---|
assertEqual(x, y) | x == y | |
assertNotEqual(x, y) | x != y | |
assertTrue(x) | bool(x) равно True | |
assertFalse(x) | bool(x) равно False | |
assertIs(x, y) | x это y | 3.1 |
assertIsNot(x, y) | x это не y | 3.1 |
assertIsNone(x) | x это None | 3.1 |
assertIsNotNone(x) | x это не None | 3.1 |
assertIn(x, y) | x в y | 3.1 |
assertNotIn(x, y) | x не в y | 3.1 |
assertIsInstance(x, y) | isinstance(x, y) | 3.2 |
assertNotIsInstance(x,y) | не isinstance(x, y) | 3.2 |
Как проверить, что функция возвращает исключение
Можно использовать TestCase.assertRaises
(или TestCase.failUnlessRaises
) из модуля unittest
.
import unittest
def broken_function():
raise Exception('Это ошибка')
class MyTestCase(unittest.TestCase):
def test(self):
with self.assertRaises(Exception) as context:
broken_function()
self.assertTrue('Это ошибка' in str(context.exception))
if __name__ == '__main__':
unittest.main()
Вывод:
.
----------------------------------------------------------------------
Ran 1 test in 0.051s
OK
Распространенные ошибки
Есть два важных момента касательно утверждений в Python, о которых нужно помнить.
- Не стоит использовать assert для валидации данных, ведь это приводит к появлению проблем с безопасностью и багов.
- Важно не писать такие утверждения, которые всегда будут истинными.
Ключевые моменты assert в Python
- Утверждение (Assertion) — это условие или булево выражение, которое должно быть истинным.
- Инструкция assert принимает выражение и необязательное сообщение.
- Инструкция assert используется для проверки типов, значений аргументов и вывода функций.
- Это также инструмент для отладки, ведь он останавливает программу при появлении ошибки.
- В первую очередь утверждения используются для уведомления разработчиков о неотслеживаемых ошибках. Они не должны сообщать об условия ошибок, таких как «файл не был найден», где пользователь может попытаться исправиться и повторить действия.
- Утверждения — это внутренняя проверка для программы. Они работают за счет объявления невозможных условий в коде. Если эти условия не проходят, значит имеется баг.
Предыдущий урок: Namedtuple
Инструкция assert
применяется для автоматического обнаружения ошибок в программах Python. Эта инструкция сделает ваши программы надежнее и проще в отладке.
По своей сути инструкция assert
представляет собой средство отладки, которое проверяет условие. Если условие утверждения assert
истинно, то ничего не происходит и ваша программа продолжает выполняться как обычно. Но если же вычисление условия дает результат ложно, то вызывается исключение AssertionError
с необязательным сообщением об ошибке.
Пример использования assert
Предположим, вы создаете интернет-магазин с помощью Python. Вы работаете над добавлением в систему функциональности скидочного купона, и в итоге вы пишете следующую функцию apply_discount()
:
def apply_discount(product, discount):
price = int(product['цена'] * (1.0 — discount))
assert 0 <= price <= product['цена']
return price
1
2
3
4
Инструкция assert
будет гарантировать, что, независимо от обстоятельств, вычисляемые этой функцией сниженные цена не может быть ниже 0 и выше первоначальной цены товара.
Давайте убедимся, что эта функция действительно работает как задумано, если вызвать ее, применив допустимую скидку. В этом примере товары в нашем магазине будут представлены в виде простых словарей, для демонстрации утверждений assert
:
shoes = {'name': 'shoes', 'price': 14999}
1
Избегая проблем с округлением денежной цены, используйте целое число для представления цены в копейках. Итак, если к этим туфлям мы применим 25 %-ную скидку, то ожидаемо придем к отпускной цене 112,49:
apply_discount(shoes, 0.25) # 112,49
1
Функция сработала. Теперь попробуем применить несколько недопустимых скидок. Например, 200%-ную «скидку», которая вынудит нас отдать деньги покупателю:
>>> apply_discount(shoes, 2.0)
Traceback (most recent call last):
File "<input>", line 1, in <module>
apply_discount(prod, 2.0)
File "<input>", line 4, in apply_discount
assert 0 <= price <= product['price']
AssertionError
1
2
3
4
5
6
7
Когда пытаемся применить недопустимую скидку, наша программа останавливается с исключением AssertionError
. Это происходит потому, что 200 %-ная скидка нарушила условие утверждения assert
.
Видно отчет об этом исключении и то, как он указывает на точную строку исходного кода, содержащую вызвавшее сбой утверждение. Если во время проверки интернет-магазина вы (или другой разработчик в вашей команде) когда-нибудь столкнетесь с одной из таких ошибок, вы легко узнаете, что произошло, просто посмотрев на отчет об обратной трассировке исключения.
Это значительно ускорит отладку и в дальнейшем сделает ваши программы удобнее в поддержке. В этом и заключается сила assert
!
Почему не применить обычное исключение?
Теперь подумаем, почему в предыдущем примере просто не применить инструкцию if
и исключение.
Дело в том, что инструкция assert
предназначена для того, чтобы сообщать разработчикам о неустранимых ошибках в программе. Инструкция assert
не предназначена для того, чтобы сигнализировать об ожидаемых ошибочных условиях, таких как ошибка «Файл не найден», где пользователь может предпринять корректирующие действия или просто попробовать еще раз.
Инструкции призваны быть внутренними самопроверками (internal selfchecks) вашей программы. Они работают путем объявления неких условий, возникновение которых в вашем исходном коде невозможно. Если одно из таких условий не сохраняется, то это означает, что в программе есть ошибка.
Если ваша программа бездефектна, то эти условия никогда не возникнут. Но если же они возникают, то программа завершится аварийно с исключением AssertionError
, говорящим, какое именно «невозможное» условие было вызвано. Это намного упрощает отслеживание и исправление ошибок в ваших программах.
А пока имейте в виду, что инструкция assert
— это средство отладки, а не механизм обработки ошибок исполнения программы. Цель использования инструкции assert
состоит в том, чтобы позволить разработчикам как можно скорее найти вероятную первопричину ошибки. Если в вашей программе ошибки нет, то исключение AssertionError
никогда не должно возникнуть.
Давайте взглянем поближе на другие вещи, которые мы можем делать с инструкцией assert
, а затем я покажу две распространенные ловушки, которые встречаются во время ее использования в реальных сценариях.
Прежде чем вы начнете применять какое-то функциональное средство языка, всегда неплохо подробнее познакомиться с тем, как оно практически реализуется в Python. Поэтому давайте бегло взглянем на синтаксис инструкции assert
в соответствии с документацией Pythonopen in new window:
инструкция_assert ::= "assert" logical_expression ["," error_message]
1
logical_expression
— это условие, которое мы проверяем,error_message
(необязательное) — это сообщение об ошибке, которое выводится на экран, если утверждение дает сбой.
Во время исполнения программы интерпретатор Python преобразовывает каждую инструкцию assert
примерно в следующую ниже последовательность инструкций:
if __debug__:
if not logical_expression:
raise AssertionError(error_message)
1
2
3
В этом фрагменте кода есть две интересные детали.
Перед тем как данное условие инструкции assert
будет проверено, проводится дополнительная проверка глобальной переменной __debug__
. Это встроенный булев флажок, который при нормальных обстоятельствах имеет значение True
, — и значение False
, если запрашивается оптимизация. Мы поговорим об этом подробнее чуть позже в разделе, посвященном «распространенным ловушкам».
Кроме того, вы можете применить error_message
, чтобы передать необязательное сообщение об ошибке, которое будет показано в отчете об обратной трассировке вместе с исключением AssertionError
. Это может еще больше упростить отладку. Например, исходный код такого плана:
if cond == 'x':
do_x()
elif cond == 'y':
do_y()
else:
assert False, ('''Это никогда не должно произойти, и тем не менее это временами происходит. Сейчас мы пытаемся выяснить причину. Если вы столкнетесь с этим на практике, то просим связаться по электронной почте с dbader. Спасибо!''')
1
2
3
4
5
6
Конечно это ужасно, но этот прием определенно допустим и полезен, если в одном из своих приложений вы сталкиваетесь с плавающей ошибкой.
Ловушки assert
Есть два важных предостережения, на которые стоит обратить внимание:
- Первое из них связано с внесением в приложения ошибок и рисков, связанных с нарушением безопасности.
- Второе касается синтаксической причуды, которая облегчает написание бесполезных инструкций
assert
.
Звучит довольно ужасно (и потенциально таковым и является), поэтому вам, вероятно, следует как минимум просмотреть эти два предостережения хотя бы бегло.
Предостережение № 1
Не используйте инструкции assert
для проверки данных!
Самое большое предостережение по поводу использования утверждений в Python состоит в том, что утверждения могут быть глобально отключены переключателями командной строки -O
и -OO
, а также переменной окружения PYTHONOPTIMIZE
в СPython
.
Это превращает любую инструкцию assert
в нулевую операцию: утверждения assert
просто компилируются и вычисляться не будут, это означает, что ни одно из условных выражений не будет выполнено.
Это преднамеренное проектное решение, которое используется схожим образом во многих других языках программирования. В качестве побочного эффекта оно приводит к тому, что становится чрезвычайно опасно использовать инструкции assert
в виде быстрого и легкого способа проверки входных данных.
Поясню: если в вашей программе утверждения assert
используются для проверки того, содержит ли аргумент функции «неправильное» или неожиданное значение, то это решение может быстро обернуться против вас и привести к ошибкам или дырам с точки зрения безопасности.
Давайте взглянем на простой пример, который демонстрирует эту проблему. И снова представьте, что вы создаете приложение Python с интернет-магазином. Где-то среди программного кода вашего приложения есть функция, которая удаляет товар по запросу пользователя.
Поскольку вы только что узнали об assert
, вам не терпится применить их в своем коде, и вы пишете следующую реализацию:
def delete_product(prod_id, user):
assert user.is_admin(), 'здесь должен быть администратор'
assert store.has_product(prod_id), 'Неизвестный товар'
store.get_product(prod_id).delete()
1
2
3
4
Приглядитесь поближе к функции delete_product
. Итак, что же произойдет, если инструкции assert
будут отключены?
В этом примере трехстрочной функции есть две серьезные проблемы, и они вызваны неправильным использованием инструкций assert
:
-
Проверка полномочий администратора инструкциями
assert
несет в себе опасность. Если утвержденияassert
отключены в интерпретаторе Python, то проверка полномочий превращается в нулевую операцию. И поэтому теперь любой пользователь может удалять товары. Проверка полномочий вообще не выполняется. В результате повышается вероятность того, что может возникнуть проблема, связанная с обеспечением безопасности, и откроется дверь для атак, способных разрушить или серьезно повредить данные в нашем интернет-магазине. Очень плохо. -
Проверка
has_product()
пропускается, когдаassert
отключена. Это означает, что методget_product()
теперь можно вызывать с недопустимыми идентификаторами товаров, что может привести к более серьезным ошибкам, — в зависимости от того, как написана наша программа. В худшем случае она может стать началом запуска DoS-атак. Например, если приложение магазина аварийно завершается при попытке стороннего лица удалить неизвестный товар, то, скорее всего, это произошло потому, что взломщик смог завалить его недопустимыми запросами на удаление и вызвать сбой в работе сервера.
Каким образом можно избежать этих проблем? Ответ таков: никогда не использовать утверждения assert
для выполнения валидации данных. Вместо этого можно выполнять проверку обычными инструкциями if
и при необходимости вызывать исключения валидации данных, как показано ниже:
def delete_product(product_id, user):
if not user.is_admin():
raise AuthError('Для удаления необходимы права админа')
if not store.has_product(product_id):
raise ValueError('Идентификатор неизвестного товара')
store.get_product(product_id).delete()
1
2
3
4
5
6
Этот обновленный пример также обладает тем преимуществом, что вместо того, чтобы вызывать неопределенные исключения AssertionError
, он теперь вызывает семантически правильные исключения, а именно ValueError
или AuthError
(которые мы должны были определить сами).
WARNING
Инструкции assert
, которые никогда не дают сбоя.
Удивительно легко случайно написать инструкцию assert
, которая всегда при вычислении возвращает истину. Вкратце проблема в следующем.
Когда в инструкцию assert
в качестве первого аргумента передается кортеж, assert
всегда возвращает True
и по этой причине выполняется успешно.
Например, это утверждение никогда не будет давать сбой:
assert(1 == 2, 'Это утверждение должно вызвать сбой')
1
Эта ситуация связана с тем, что в Python непустые кортежи всегда являются истинными. Если вы передаете кортеж в инструкцию assert
, то это приводит к тому, что условие assert
всегда будет истинным, что, в свою очередь, приводит к тому, что вышеупомянутая инструкция assert
станет бесполезной, потому что она никогда не сможет дать сбой и вызвать исключение.
По причине такого, в общем-то, не интуитивного поведения относительно легко случайно написать плохие многострочные инструкции assert
. Например, представьте, что в одном из ваших модульных тестов имеется приведенное ниже утверждение:
assert (
counter == 10,
'Это должно было сосчитать все элементы'
)
1
2
3
4
На первый взгляд этот тестовый случай выглядит абсолютно приемлемым. Однако он никогда не выловит неправильный результат: это утверждение assert
всегда будет давать истину, независимо от состояния переменной counter
. И в чем же тут дело? А в том, что оно подтверждает истинность объекта-кортежа.
Более свежие версии Python 3 для таких сомнительных инструкций assert
показывают синтаксическое предупреждение.
Между прочим, именно поэтому всегда следует выполнять быстрый тест при помощи своих модульных тестовых случаев. Прежде чем переходить к написанию следующего, убедитесь, что они действительно не срабатывают.
Инструкции assert — резюме
Несмотря на данные выше предостережения, я полагаю, что инструкции assert
являются мощным инструментом отладки, который зачастую недостаточно используется разработчиками Python.
Понимание того, как работают инструкции assert
и когда их применять, поможет писать программы Python, которые будет легче сопровождать и отлаживать.
Это великолепный навык, который стоит освоить, чтобы прокачать знания Python до более качественного уровня и стать всесторонним питонистом.Это позволит сэкономить бесконечные часы, которые приходится тратить на отладку.
Ключевые выводы
-
Инструкция
assert
— это средство отладки, которое проверяет условие, выступающее в качестве внутренней самопроверки вашей программы. -
Инструкции
assert
должны применяться только для того, чтобы помогать разработчикам идентифицировать ошибки. Они не являются механизмом обработки ошибок периода исполнения программы. -
Инструкции
assert
могут быть глобально отключены в настройках интерпретатора.
In Python, the assert statements make sure a condition is met before continuing the execution of the program.
For example, let’s assert that a discounted price does not go below 0 nor surpass the original price:
def discount(price, dc): new_price = int(price * (1.0 - dc)) assert 0 <= new_price, "Price is less than 0." assert new_price <= price, "Price is more than original price" return new_price
Calling this function with an input that makes the assertion fail will lead to the code crash with a clear error:
price = 125 dc = 1.3 # 130% discount discounted_price = discount(price, dc) print(discounted_price)
Output:
Traceback (most recent call last): File "example.py", line 10, in <module> discounted_price = discount(price, dc) File "example.py", line 3, in discount assert 0 <= new_price <= price AssertionError: Price is less than 0.
You should only do assertions when debugging your code. Do not leave assert statements in a production environment!
In this guide, you will learn how to adapt assertion as one of your debugging routines. You are going to see some real-life use cases of assertion in Python. And you will also see an example of when assertion can be really dangerous.
Assertion in Python
Debugging Python code helps you track down bugs that cause issues in your programs.
Every developer knows how daunting the process of debugging can sometimes be.
When you debug a program, you might want to test for specific conditions. More specifically, if a condition is not met, the program should throw an error instead of continuing. This is one common way to narrow down the root cause of a bug and save time when debugging code.
Python’s built-in assert statement is the right solution for this. The assert statement is used to handle errors during the debugging phase.
You can use Python’s built-in assert statement to debug your code.
Assertion informs a developer of an unrecoverable error. Thus, the assertion is not intended to point out regular error conditions like “File not found”.
You should use assertions to declare conditions that should be impossible in your code. If one of these impossible conditions does not hold, it means there’s a bug in your code.
Asserting works so that if a condition you assert is True, the program continues. But if the condition evaluates False, an error called AssertionError is thrown.
If your program code has no bugs, these impossible conditions never become True. But if one occurs, the program crashes with an AssertionError telling you exactly which “impossible” condition was triggered.
Use Assertions Cautiously in Python
Use assertions cautiously, only when debugging your code.
Assertion helps you find the root cause of a bug. Unlike regular runtime errors in Python, assertions have to be manually defined by a developer.
Unlike traditional code errors, It is possible to completely turn off assertion statements in a Python interpreter. This can lead to problems if you rely on assertions in the code. If you have disabled assertions, the conditions checked don’t get triggered and the faulty code is able to proceed in execution.
Now that you know about the assertion in Python on a theoretical level, let’s take a look at some concrete examples.
The assert Statements in Python
The assert statement checks if a condition evaluates True.
- If it does, the program keeps running.
- If it does not, the program returns an AssertionError and the program stops executing.
In Python, assert is a reserved keyword. It follows this syntax:
assert condition, message
Where the two parameters are:
- condition: the condition against which you test your code.
- message: the message you want to show if the assertion fails. This is an optional parameter that you do not need to specify. Leave it out if you don’t need one.
Similarity to If Statements
In case you’re wondering, using assert is roughly equal to the following traditional if statement in Python:
if not condition: raise AssertionError(message)
One difference to this is that the assert statements can be disabled by running a Python program with an -O flag.
But the main difference is that the assert statement means checking if a condition is not True only in the debug mode.
So in reality, using the assert statement is equivalent to the following:
if __debug__: if not condition: raise AssertionError(message)
Even though this is a subtle difference, it highlights why you should only use assertion in debugging. If the __debug__ mode False, assertions do not get triggered, and the code with potential bugs is free to run.
Assert Example in Python
Let’s build a simple discount calculator in Python. This function takes a price of an item and a discount percentage as a fractional number.
Here is the code:
def discount(price, dc): new_price = int(price * (1.0 - dc)) assert 0 <= new_price, "Price is less than 0." assert new_price <= price, "Price is more than original price" return new_price
In this function, the assert statements make sure:
- The discounted price is not less than 0.
- The discounted price is not more than the original price.
Now, let’s test how it works by calling the discount() function:
price = 125 dc = 0.3 discounted_price = discount(price, dc) print(discounted_price)
Output:
87
So it seems to work as we intended.
Now, let’s input an invalid discount to the discount() function. As you may know from maths, it would make no sense to give a discount exceeding 100% (or 1.0 when talking about fractional discounts).
Let’s see what happens with a discount of over 100%.
For example, let’s say the discount is 130% (1.3 in fractional):
price = 125 dc = 1.3 discounted_price = discount(price, dc) print(discounted_price)
Output:
Traceback (most recent call last): File "example.py", line 10, in <module> discounted_price = discount(price, dc) File "example.py", line 3, in discount assert 0 <= new_price <= price AssertionError: Price is less than 0.
As you see, applying an invalid discount crashes the program with an AssertionError. With the help of this assertion error and the traceback, it is easy to track down what went wrong in our code.
AssertionError: Price is less than 0.
In this case, it is the new price that dropped below zero, which means the discount was too high. This is how the assert statement can help you debug your code.
Assert Statement vs If Statement in Python
It may seem like the assert statement and the if statement does the same thing. They both check if a condition is True or False.
But there are key differences between assert and if statements in Python:
- An if statement is a part of the logic of the code. The assert statement is used to validate input when debugging code.
- If the if statement’s condition is False, the code continues from where we want it to. When an assert statement is False, an error is thrown and the code execution stops completely.
- An if statement is always there. The assert statements can be disabled and not considered by the Python interpreter. This makes faulty code run.
Assert Multiple Conditions
The point of assertion is to track down bugs and impossible conditions in your program more easily.
You can make multiple assertions in a single assert statement in Python.
But if you do this, you only make your job harder. This is because you cannot determine which one of the conditions caused the bug.
Anyway, here is an example of multiple assert conditions:
assert 1 + 1 == 2 and 1 + 2 == 5, "Error occurred somewhere"
Output:
AssertionError: Error occurred somewhere
But as you can tell from the output, the error took place somewhere. Thus, you have to inspect the assertion conditions to figure out what went wrong. This is why it doesn’t make sense to use multiple conditions when asserting.
Let’s re-format the above by splitting the assertion into two separate statements:
assert 1 + 2 == 3, "Issue with 1 + 2 == 3" assert 1 + 2 == 5, "Issue with 1 + 2 == 5"
Output:
AssertionError: Issue with 1 + 2 == 5
Now you can instantly tell that it was the last assertion that failed without having to spend a second thinking about it.
Python Assert Statements on Multiple Lines
Although you should try to keep your assert message short and concise, you might want to know a neat little trick you can do to expand the messages to multiple lines.
- Wrap the message around a set of parentheses.
- Break the lines as desired.
- Wrap each line into a set of single or double quotes.
For instance:
assert 1 + 1 == 3, ( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " "Donec id magna id lectus gravida finibus. In gravida, mauris " "condimentum rhoncus sagittis, mi nisl lacinia tortor, non interdum " "nisl diam et lectus. Sed vitae ante vestibulum, volutpat neque non, " "vehicula erat. Nullam aliquam risus orci, blandit varius metus pulvinar id." )
Output:
AssertionError: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec id magna id lectus gravida finibus. In gravida, mauris condimentum rhoncus sagittis, mi nisl lacinia tortor, non interdum nisl diam et lectus. Sed vitae ante vestibulum, volutpat neque non, vehicula erat. Nullam aliquam risus orci, blandit varius metus pulvinar id.
Python Assert Statement—Do You Need Parentheses?
Be careful with parentheses and assert. In Python 3, assert is a statement, not a function. This means it does not take parenthesis!
Using assert with parenthesis like this is wrong:
assert(1 + 1 == 3, "An error occurred")
Instead, you should use it without parenthesis like this:
assert 1 + 1 == 3, "An error occurred"
The reason why the parenthesis doesn’t work is that assertion with parenthesis always evaluates True. This is because, in the boolean context, any non-empty tuple evaluates True.
For instance, if you convert a tuple (1, 2) to a boolean, you get True:
bool((1, 2)) # Returns True
Similarly:
bool((1+1 == 3, "An error")) # Also returns True
As a result, if you write the assert statement with parenthesis, you are essentially just saying assert(True).
The only way you can use parenthesis with the assert statement is by wrapping the condition to parenthesis but leaving the message out.
For example:
assert (1 + 1 == 3), "An error occurred"
Output:
AssertionError: An error occurred
This assertion works fine because the assertion is not applied to a tuple. Instead, it is applied to the condition.
Assertions Are Not for Data Validation in Python
The assert statements can be turned off in Python. So do not rely on assert statements for data validation. This can lead to big problems as these checks are omitted.
This is not a mistake by Python developers. Instead, it is an intentional decision. It is really dangerous to use assert as a shorthand way for data validation.
Imagine you’re building a social media app with Python. Somewhere in the code, you want to remove users as an administrator of the app. To do this, you assert that the current user is the administrator that has the privilege to delete users before actually deleting one.
Here is what your code would look like:
def delete_account(user_id, user): assert user.is_admin(), "Must be admin to delete" users.find_user(user_id).delete()
If you now take a closer look at this function, what do you think happens when the assertions are disabled?
It causes a serious issue. Any user can delete any account given a user ID. This happens because the admin check is now a null operation due to assertions being disabled.
This kind of behavior would be catastrophic for your application.
So do not use assertions to do data validation. You can use a regular if statement instead.
For instance:
def delete_account(user_id, user): if not user.is_admin(): raise AuthError("Must be admin to delete") users.find_user(user_id).delete()
This approach is also better as it:
- Does not pose a security risk
- Raises an AuthError, instead of a mysterious AssertionError.
How to Disable Assertions in Python
You can disable assertions in Python by executing your program file with the -O flag. This sets the __debug__ to False.
For instance, given a Python program in a file called example.py you can disable the assertions by running the code with:
python -O example.py
Now no assert statements are triggered regardless of whether there was a bug or not.
Conclusion
Today you learned how Python assert statements works.
The assert statement in Python is meant to debug your code—not to handle runtime errors.
An assert statement can be thought of as a sanity check. It helps developers make sure what they think is true really is true.
Thanks for reading. Happy coding!
Further Reading
- Python Tricks
- How to Write to a File in Python
- The with Statement in Python
About the Author
- I’m an entrepreneur and a blogger from Finland. My goal is to make coding and tech easier for you with comprehensive guides and reviews.
Recent Posts
Python – это универсальный язык программирования, позволяющий выполнять практически любые вычислительные задачи. Он обладает огромным количеством встроенных модулей, функций и ключевых слов, которые могут быть чрезвычайно полезны. Один из таких операторов – assert
. Именно о нем мы поговорим сегодня.
Оператор assert
– это встроенный оператор или ключевое слово в Python, используемое для отладки кода. Это своего рода проверка, которая исследует функциональность вашего кода.
Оператор assert
работает как логическое выражение, проверяя, является ли заданное условие истинным или ложным. Если условие истинно, то ничего не происходит и выполняется следующая строка кода. Если же условие ложно, оператор assert
останавливает выполнение программы и выдает ошибку. В этом случае assert
работает как ключевое слово raise
и выводит исключение. Исключение, вызванное оператором assert
, также называется AssertionError
.
В этой статье мы рассмотрим оператор assert
в Python и разберем принципы его работы на нескольких примерах.
Синтаксис
Синтаксис использования оператора assert
следующий:
assert <condition>
Также можно добавить дополнительное сообщение, которое будет выводиться при ошибке. В таком случае синтаксис assert
будет следующий:
assert <condition>, <message>
Примеры
Чтобы разобраться, как пользоваться оператором assert на практике, давайте обсудим несколько примеров.
При выполнении операции деления следует учитывать, что на ноль делить нельзя. Если делитель будет равен нулю, программа вызовет ошибку ZeroDivisionError
. Чтобы проверить, не равен ли делитель нулю, можно вставить оператор assert
. Давайте реализуем это на Python.
num1 = 10 num2 = 0 assert num2 != 0, "The divisor is zero"
В assert мы указали условие, что num2
(делитель) не должен быть равен нулю. Данное условие не выполняется, потому что значение num2
равно нулю. Интерпретатор Python выдает ошибку AssertionError
вместе с добавленным нами сообщением «The divisor is zero» («Делитель равен нулю»).
Теперь давайте изменим значение num2
и выполним нашу программу снова. На этот раз утверждение assert
истинно, так что в этом случае ничего не произойдет. Ошибка не появляется, вместо этого выполняется следующая строка. Таким образом, на экран выведется результат деления — 1.0.
num1 = 10 num2 = 10 assert num2 != 0, "The divisor is zero" print("The result is:", num1/num2)
Теперь давайте разберем другой пример. Напишем условие assert
для проверки корректности строки.
val = "Pythonist" assert val != "Pythonist", "The condition is false"
Условие не выполняется, поэтому выводится AssertionError
.
Можно привести и альтернативный вариант примера со строкой:
val = "hello" assert val == "Pythonist", "The variable value is not equal to Pythonist"
Значение переменной val
– hello. Это означает, что условие ложно, и программа выведет ошибку AssertionError
с нашим комментарием — «The variable value is not equal to Pythonist».
Заключение
Оператор assert
– это встроенный оператор в Python, позволяющий отлаживать код. Этот оператор принимает условие и необязательное сообщение, которое выводится, если условие assert ложно. В этом случае инструкция assert выводит AssertionError. Если условие истинно, то ничего не происходит и выполняется следующая строка кода. В этой статье мы подробно объяснили принцип работы оператора assert
и разобрали его на примерах.