Addaly is in open beta. Things will change, and AI answers can be wrong — check anything that matters.

Python, From Zero, For AI

From your first line of code to your first API call.

Lesson 34 of 897 min

Assertions: stating what must be true, and where they vanish

What an assert says

python
def merge(left, right):
    merged = combine(left, right)
    assert len(merged) == len(left) + len(right), (
        f"merge lost rows: {len(merged)} from {len(left)}+{len(right)}"
    )
    return merged

An assert says: at this point in the program, this must be true, and if it is not, my code is wrong. It is a claim about the program, not about the data.

The distinction is the whole lesson. people < 1 in the earlier lesson was a claim about the caller's input, and it got a raise ValueError. len(merged) == len(left) + len(right) is a claim about your own function's correctness, and it gets an assert.

The single best place for one is right after a join or a merge, where the row count is the thing that quietly goes wrong. A merge that duplicates rows produces a plausible, larger, entirely wrong dataset, and nothing else will tell you.

The part that matters most

Assertions disappear when Python is run with -O.

bash
python3 -O script.py

-O sets __debug__ to False and removes every assert statement at compile time. It is also implied by the PYTHONOPTIMIZE environment variable, which some deployment images set without telling you.

So this is a security hole:

python
assert user.is_admin, "not authorised"     # gone under -O
delete_everything()

And so is this:

python
assert age >= 18                            # gone under -O

Never use assert for validating input, checking permissions, or anything that must happen in production. Those get an if and a raise. Use assert for internal consistency checks whose failure means a programmer made a mistake.

The same check, written two ways, run two wayspython app.pypython -O app.pyassert cond, msgif not cond: raiseCheckedAssertionErrorGonecompiled awayCheckedyour exceptionCheckedyour exception-O sets __debug__ to False and removes every assert at compile time, and it is implied by thePYTHONOPTIMIZE variable that some deployment images set for you. So assert is for claims about yourown code — a row count after a merge — and never for validating input or checking permissions.
The same check, written two ways, run twowayspython app.pypython -O app.pyassert cond, msgCheckedAssertionErrorGonecompiled awayif not cond: raiseCheckedyour exceptionCheckedyour exception-O sets __debug__ to False and removes every assertat compile time, and it is implied by thePYTHONOPTIMIZE variable that some deployment imagesset for you. So assert is for claims about your owncode — a row count after a merge — and never forvalidating input or checking permissions.

Whether -O is ever actually used is a fair question — in practice it is rare, and plenty of production code has assertions that always run. That is exactly why the rule is stated as an absolute. You do not control the flags on the machine your code eventually runs on, and a check that might not execute is not a check.

The tuple bug

python
assert (total > 0, "total must be positive")

This always passes. A non-empty tuple is truthy, so the assertion tests the tuple object, not the comparison. The message never appears and the check never fails.

The comma goes outside the expression:

python
assert total > 0, "total must be positive"

Modern Python emits a SyntaxWarning for the tuple form, and ruff flags it. It is still worth recognising by eye, because it appears in a lot of older code and it silently disables the check.

Assertions in tests are a different thing

pytest uses the assert statement as its checking mechanism, and there the concern above does not apply — tests are never run with -O, and pytest rewrites assertions to produce detailed failure output. In a test, assert result == expected is exactly right, and the next lesson covers what it prints when it fails.

Invariants worth asserting

The useful ones state a relationship that should hold no matter what the data is:

python
assert not df.duplicated(subset="id").any(), "duplicate ids after merge"
assert abs(sum(weights) - 1.0) < 1e-9, f"weights sum to {sum(weights)}"
assert set(train.index) & set(test.index) == set(), "train/test overlap"

That last one is the check that catches the most expensive mistake in machine learning, and it is one line. The data module later in this course explains why an overlap between training and test rows invalidates every number you report.

The cheaper cousin: a check that stays

For an invariant you want enforced in every environment, write it out:

python
if len(merged) != len(left) + len(right):
    raise RuntimeError(
        f"merge lost rows: {len(merged)} from {len(left)}+{len(right)}"
    )

Three lines instead of one, and it cannot be optimised away. Use this form for anything protecting data integrity in a pipeline that runs unattended, and keep assert for the checks you are happy to lose in exchange for brevity while developing.

Where they do not belong

  • Anywhere a user's mistake is expected. Users are not bugs.
  • As documentation. If the assertion is only there to say what the code does, write a docstring.
  • Inside a hot loop, if the check is expensive. An assertion that doubles the runtime will be deleted by somebody, and then it is protecting nothing.
A good rule of thumb: if you would be embarrassed for a user to see the message, it is an assertion. If the message is advice to the user, it is a raise.

The one thing to keep

An assert states a claim about your own code's correctness and is removed entirely under `python -O`, so validation of input, permissions or anything a user can cause must use an `if` and a `raise`.

Before you move on

A service checks an uploaded file's size with `assert size <= MAX_UPLOAD, "file too large"` and it works in every test. What is the risk in production?

Pick the one you would defend. Nobody sees your answer.

No ads. No data sale. No public scores on people. Ever.

© 2026 Addaly