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.