DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
Getting Started
01
What is Python and Why Learn It in 2026
02
Installing Python on Windows, Mac, and Linux
03
Setting up VS Code / PyCharm for Python
04
Understanding the Python Interpreter and REPL
05
Writing and Running Your First Python Program
06
Python Syntax and Indentation Rules
Variables and Data Types
01
Variables and Naming Conventions in Python
02
Numbers in Python (int, float, complex)
03
Strings and String Methods in Python
04
Booleans in Python
05
Type Conversion / Casting in Python
06
Understanding the None Type in Python
Operators
01
Arithmetic Operators in Python
02
Comparison Operators in Python
03
Logical Operators in Python
04
Assignment Operators in Python
05
Bitwise Operators in Python
06
Identity (is) and Membership (in) Operators
07
Walrus Operator (:=) in Python
Input, Output & Strings
01
input() and print() Functions in Python
02
f-strings and String Formatting in Python
03
String Slicing and Indexing in Python
04
Common String Methods (split, join, strip, replace)
05
Escape Characters in Python Strings
Control Flow
01
if, elif, else Statements in Python
02
Nested Conditionals in Python
03
for Loops in Python
04
while Loops in Python
05
break, continue, and pass Statements
06
Loop with else Clause in Python
07
match-case (Structural Pattern Matching)
Data Structures
01
Python Lists: Creation, Indexing, Slicing, Methods
02
Python Tuples and Immutability
03
Python Sets and Set Operations
04
Python Dictionaries: Keys, Values, and Methods
05
List Comprehensions in Python
06
Dictionary & Set Comprehensions in Python
07
Working with Nested Lists and Dictionaries
Functions
01
Defining and Calling Functions in Python
02
Function Arguments: Positional, Keyword, Default
03
*args and **kwargs in Python
04
Return Statements in Python Functions
05
Lambda Functions in Python
06
Recursion in Python
07
Variable Scope: Local, Global, Nonlocal
08
Docstrings and Function Annotations
Functional Programming Basics
01
map(), filter(), and reduce() in Python
02
Closures in Python Explained
03
Python Decorators: Basic to Advanced
04
Generators and yield in Python
Object-Oriented Programming (OOP)
01
Classes and Objects in Python
02
The init Constructor in Python
03
Instance vs Class Variables in Python
04
Inheritance in Python (Single, Multiple, Multilevel)
05
Polymorphism in Python
06
Encapsulation in Python
07
Abstraction in Python with the abc Module
08
Magic/Dunder Methods in Python
09
Static and Class Methods in Python
10
The @property Decorator in Python
11
Python Dataclasses Explained
Error Handling
01
try, except, finally in Python
02
Handling Multiple Exceptions in Python
03
Raising Custom Exceptions in Python
04
Understanding Python's Exception Hierarchy
05
Assertions in Python
File Handling
01
Reading and Writing Text Files in Python
02
Working with the with Statement in Python
03
Working with CSV Files in Python
04
Working with JSON Files in Python
05
Working with Directories (os, pathlib) in Python
Modules and Packages
01
Importing Built-in Modules in Python
02
Creating Your Own Modules in Python
03
Understanding Packages and init.py
04
Python Standard Library Overview
05
Installing Packages with pip
Advanced Core Concepts
01
Iterators and Iterables in Python
02
Context Managers in Python (with, contextlib)
03
Multithreading Basics in Python
04
Multiprocessing Basics in Python
05
Asyncio Basics: async/await in Python
06
Memory Management and Garbage Collection in Python
07
Type Hints in Python (typing module)

Assertions in Python

The python assert statement is a tool for sanity-checking your own assumptions as a developer — not a substitute for proper exception handling. This article covers what python assertions actually do at runtime, when to reach for assert versus a raised exception, and a genuinely important gotcha: assertions can be silently stripped out of your program entirely, under conditions you need to know about.

What Is an Assertion?

assert is a statement for setting sanity checks in your code — it checks a condition and raises an AssertionError if that condition turns out to be false.

The mental model

Assertions document assumptions the developer believes must always be true. They exist to test those assumptions — verifying that the conditions your code relies on to function correctly genuinely hold, rather than silently assuming they do and potentially producing wrong results later if they don't.

Basic syntax
assert condition, "optional error message"
def divide(a, b):
    assert b != 0, "b must not be zero"
    return a / b

print(divide(10, 2))   # 5.0

Here, the assertion documents an assumption: "by the time this line runs, b should never be zero." If that assumption ever turns out to be wrong, the assertion fails loudly, immediately, rather than letting a confusing ZeroDivisionError (or worse, silently wrong behavior) surface somewhere else.

How assert Behaves at Runtime

If the condition is True

Execution simply continues, completely silently — an assertion that passes has zero visible effect on your program whatsoever:

assert 1 + 1 == 2   # nothing happens at all — the assertion passed silently
print("Still running")   # Still running
If the condition is False

Python immediately raises an AssertionError, halting the program unless it's caught by a surrounding try/except:

assert 1 + 1 == 3, "Math is broken"
# AssertionError: Math is broken
Why the optional message matters

Compare these two failures:

assert user.is_active
# AssertionError

assert user.is_active, f"Expected user {user.id} to be active, but is_active was False"
# AssertionError: Expected user 42 to be active, but is_active was False

The first tells you that something failed, but nothing about what or why. The second turns a mysterious crash into an immediately understandable one — pointing directly at exactly which assumption broke, and with what actual data. Always including a descriptive message is one of the cheapest, highest-value habits you can build around using assertions at all.

assert vs. Exceptions: Choosing the Right Tool

The core distinction

This is genuinely the most important thing to understand about assertions: assert is for developer-defined conditions that should never be false — bugs in your own code's logic, essentially. Exceptions handle anticipated runtime errors — situations that can legitimately occur due to external, real-world conditions: bad user input, a missing file, a failed network request.

A simple decision test

Ask yourself: if this check fails, does it indicate a bug in the code itself (something that should be caught and fixed during development), or could it legitimately be triggered by external, real-world conditions a normal user might reasonably encounter?

# Good use of assert — a bug in the code itself would trigger this,
# not any normal user action
def calculate_average(numbers):
    assert isinstance(numbers, list), "numbers must be a list"
    return sum(numbers) / len(numbers)

# Bad use of assert — this IS something a normal user could trigger,
# and should be handled with a real exception, not silently disappear-able
def set_age(age):
    assert age >= 0, "age cannot be negative"   # WRONG TOOL — see Section 4 for why

If removing the check could let a user cause a security issue, corrupt data, or otherwise reach a genuinely broken state through entirely normal, expected usage — that's a strong signal you need raise and a proper exception, not assert. Section 4 explains exactly why this distinction matters so much in practice, beyond just stylistic preference.

Never validate user input or external data with assert

To state this plainly: assertions should never be used to validate user input, data coming from a file, an API response, or anything else originating outside your own program's control. That's precisely what proper exception handling — raise ValueError(...), custom exceptions (covered in the earlier custom exceptions article), and explicit validation — exists for. Assertions are meant for checking things you, the developer, believe should always be true given correct code — not things that depend on the behavior of users or external systems you don't control.

The Critical Gotcha: Assertions Can Be Disabled

Running Python in optimized mode strips assertions entirely

This is the single most important, and most commonly overlooked, fact about Python's assert statement: running Python with the -O (or -OO) flag, or with the PYTHONOPTIMIZE environment variable set, strips out every assert statement in your program entirely — as if they were never written at all.

Under -O, every single assert in my_script.py is completely skipped — not caught, not disabled in the sense of "always passing" — genuinely removed from execution, as though the line simply weren't there.

The practical danger

This is precisely why the earlier guidance in Section 3 matters so much in practice: never use assertions for critical validations in production code. Many deployment tools and production environments run Python with optimization flags for performance reasons — meaning any check you've placed inside an assert statement, believing it protects your application, can simply vanish in production, with zero warning, zero error, and zero indication that the check ever stopped running at all.

def withdraw(account, amount):
    assert amount <= account.balance, "Insufficient funds"   # DANGEROUS in production
    account.balance -= amount

If this code runs under -O, the balance check silently disappears — withdraw() will happily let someone withdraw more than their actual balance, with no error raised anywhere, because the assertion protecting against exactly that never actually ran at all. The fix is straightforward: use a real, raised exception for anything that genuinely needs to be enforced regardless of how Python happens to be invoked:

def withdraw(account, amount):
    if amount > account.balance:
        raise ValueError("Insufficient funds")
    account.balance -= amount

This version behaves identically whether or not -O is used — raise statements are never stripped out under any optimization flag, unlike assert.

The debug flag

Python exposes a built-in __debug__ variable, which is True under normal execution and False when running under -O/-OO. This is useful for wrapping expensive, assertion-only logic that should also disappear entirely in optimized mode, alongside the assertions it supports:

if __debug__:
    # expensive validation logic, only relevant for catching development-time bugs
    expensive_debug_check(data)

This pattern is genuinely useful if you have costly diagnostic logic that only exists to support assertions during development, and that you're comfortable disappearing entirely in an optimized, production deployment — consistent, deliberate behavior, rather than the accidental, silent gap that misusing assert for real validation would create.

Practical Use Cases and Best Practices

Good fits for assertions
  • Validating internal function preconditions during development — confirming that a function received the kind of input it genuinely expects, as a way of catching bugs in calling code early, during development and testing.

  • Confirming invariants in data pipelines — checking that data transformed through several processing steps still has the shape or properties it's supposed to have at each stage, catching a broken transformation step immediately rather than letting corrupted data flow further downstream silently.

  • Quick, informal checks before a full test suite exists — a lightweight way to catch obviously broken assumptions early in a project's life, before you've invested in more formal testing infrastructure.

The pytest connection

Here's a genuinely important, practical reason assert is worth knowing well beyond casual debugging: the popular pytest testing framework uses plain assert statements directly as its primary way of writing test checks, rather than requiring special assertion methods the way some other testing frameworks do:

def test_divide():
    assert divide(10, 2) == 5.0
    assert divide(9, 3) == 3.0

pytest cleverly rewrites these plain assert statements internally to produce detailed, helpful failure output — showing you the actual values involved when a test fails — all from ordinary Python assert syntax, no special test-specific assertion methods required. This makes assert genuinely useful well beyond informal debugging, tying directly into how real-world test suites are commonly written.

Best practices, recapped
  • Keep assertions simple and side-effect-free. An assertion's condition should be a pure check — it shouldn't modify any state, since (as covered in Section 4) it might not even run at all under optimized mode, and code shouldn't depend on side effects from something that could silently vanish.

  • Always include a descriptive message. As shown in Section 2, this is what turns a mysterious AssertionError into an immediately actionable one.

  • Never wrap an assert in a try/except to "handle" it. If you find yourself catching AssertionError and responding to it gracefully, that's a strong signal the check was never actually a developer-assumption check in the first place — it was really validating something that can legitimately happen, and belongs as a proper raised exception instead. A failed assertion should be allowed to surface as a genuine, visible bug report — not quietly caught and worked around.

PREVIOUSNEXT LESSON