Decorators and Function Wrapping
When the original function contains an assert statement whose condition is False, Python raises an AssertionError immediately.
Try it: A Retry Decorator, Step by Step
What a decorator does: @retry(max_attempts) turns a function into a wrapper that calls the original. When the original's assertion fails, the wrapper catches the AssertionError, logs the attempt and runs the WHOLE function again (repeating its side effect) until it succeeds or runs out of attempts, then re-raises (with a traceback whose deepest frame is the assert) or returns a default.
How it works
- @retry(max_attempts=3) is evaluated first: retry(3) returns decorate; decorate(save_reading) returns wrapper; the name save_reading is bound to the wrapper.
- Calling save_reading(42) runs the wrapper, which loops over attempts and calls the original function inside try.
- A false assert raises AssertionError immediately; the original stops and the exception moves up to the wrapper's except block.
- The wrapper logs the failed attempt and starts a new, complete execution of the original: side effects before the assert (saved.append) happen again.
- After the last allowed attempt the wrapper's design decides: raise last_error (the traceback's deepest frame is the assert line; the wrapper frames only caught and re-raised it) or return None.
Default run (36 steps): Line 1: def retry(max_attempts): creates the decorator factory and binds the name retry. Nothing inside it runs yet. … The program finished: the wrapper returned 'saved' from attempt 3. The side effect ran 3 times for one reading: a retried function should be idempotent.
Simplified: One fixed program; you choose the per-attempt sensor results, max_attempts (1-5), whether the write comes before or after the check, and raise vs return None. Your settings are never executed: the lab's model reproduces CPython 3.12's line-by-line execution, printed output and traceback text for all 640 possible programs (checked against real runs). The "database" is a Python list.
Loading the simulation…
The First Failure
A decorated function has an extra layer between its caller and its original body. That layer can observe an exception, record what happened, and decide whether to let the exception reach the caller or to execute the function again. When the original function contains an assert statement whose condition is False, Python raises an AssertionError immediately. The current function stops at that point, and the error moves up the call stack unless an exception handler catches it.
What do you think happens?
A decorated function reaches an assert statement whose condition is False. What happens next?
Reveal answer
An AssertionError is raised and the current function stops.
The error then propagates through the call stack. If a retry decorator is handling exceptions, that decorator can catch the AssertionError before it reaches the caller.
From Assertion to Exception
An assert statement checks a condition at a particular point in a function. If the condition is true, execution can continue past the assertion. If the condition is False, Python raises an AssertionError immediately. This does not merely mark the condition as unsuccessful: it stops the current function and begins exception propagation.
The decorator does not change the fact that the assertion fails. Instead, it can change what happens after the failure. Its exception-handling layer may catch the AssertionError, log the failed attempt, and choose whether to retry or propagate the exception.
The Decorator Boundary
A retry decorator sits between the caller and the wrapped function. The caller starts the decorated operation, the decorator invokes the wrapped function, and the wrapped function either completes or raises an exception. On success, control returns through the decorator to the caller. On failure, the decorator's exception handler gets an opportunity to log the failure and decide what happens next.
A Failed Assertion Inside a Wrapper
Trace the control flow when a caller starts a decorated function, the function reaches a false assertion, and the decorator catches the resulting AssertionError.
- 11. The caller starts the decorated operation
Control first enters the decorator layer rather than moving directly from the caller to the original function body.
- 22. The decorator invokes the wrapped function
The decorator passes execution to the original function so that the function can perform its work.
- 33. The assertion condition is False
Python raises an
AssertionErrorimmediately, and the wrapped function stops at the failed assertion. - 44. The error reaches the decorator
The exception propagates through the call stack to the decorator's exception handler, which can catch it.
- 55. The decorator chooses the next action
A retry design can log the failed attempt and re-execute the entire wrapped function, subject to its maximum number of attempts.
The decorator is the control boundary that can intercept the AssertionError before the caller receives it.
Retrying the Whole Operation
A retry decorator can catch exceptions, including AssertionError, log the failure together with the attempt number, and automatically re-execute the function up to a maximum number of attempts. The retry is a new execution of the entire wrapped function. It is not a jump back to only the assertion line.
Attempt One and Attempt Two
A decorated function fails on its first execution and succeeds on its second. Predict the sequence of events.
- 1First execution
The decorator invokes the wrapped function. The function begins, reaches a failing condition, and raises an exception.
- 2Failure handling
The decorator catches the exception and logs a message identifying the failed attempt, such as the first attempt out of a configured maximum.
- 3Second execution
The decorator invokes the entire wrapped function again. The function starts from the beginning of its wrapped execution rather than from only the failed line.
- 4Successful return
When the second execution succeeds, control returns through the decorator to the caller.
The observable sequence is function execution, exception, decorator logging, full function re-execution, and successful return.
Reading the Traceback
When an assertion fails, Python generates a traceback. The traceback identifies the file name and line number associated with the failed assertion and shows the function where the failure occurred. It also displays the call path that led to the failure.
Read the traceback from the bottom upward. The deepest frame, listed last, identifies where the exception actually originated. Decorator frames belong to the call stack, but they are not necessarily the source of the error. Use the file name and line number in the deepest frame to locate the failed assertion in the source code.
Separating the Wrapper from the Failure
A traceback contains a caller frame, a decorator frame, and a final frame showing the wrapped function's assertion line. Which frame identifies the original failure?
- 1Identify the deepest frame
The deepest frame is the last frame listed in the traceback.
- 2Check its location
Look for the file name and line number shown in that frame.
- 3Distinguish handling from origin
The decorator frame explains where the exception was intercepted or handled. The deepest frame identifies where the assertion actually became false.
The final, deepest traceback frame points to the assertion's source location.
What the Wrapper Contains
Function wrapping describes the relationship in which a decorator places a wrapper layer around an original function. The wrapper becomes the path used by the caller, while the original function remains the operation that the wrapper invokes. This arrangement lets the decorator intercept execution before and after the wrapped function, including the moment when the wrapped function raises an exception.
The important mental model is not that the decorator removes the original function's behavior. It inserts a control layer around that behavior. The caller enters through the wrapper, and the wrapper decides how to invoke the original operation and how to respond if that operation fails.
Mistakes in Control Flow
Common mistake Assuming a false assertion is ignored
What it looks likeThe function continues after an assert condition evaluates to False.
Why it failsA false assert condition raises an
AssertionErrorimmediately and stops the current function.Do this insteadTrace the AssertionError from the assertion to the surrounding exception handler.
Common mistake Assuming the decorator is the source of every failure
What it looks likeThe traceback contains a decorator frame, so the decorator must contain the faulty assertion.
Why it failsDecorator frames are part of the call stack, but the deepest frame shows where the exception actually originated.
Do this insteadInspect the last traceback frame and use its file name and line number to find the failed assertion.
Common mistake Assuming a retry resumes at the failed line
What it looks likeAfter an assertion fails, only that assertion is evaluated again.
Why it failsA retry re-executes the entire wrapped function.
Do this insteadStart the trace at the beginning of a new function execution and account for repeated side effects.
Common mistake Assuming every retry eventually succeeds
What it looks likeThe decorator always returns a successful result after catching an exception.
Why it failsA retry process is limited by a maximum number of attempts. After the limit, the decorator may raise the last exception or return a default value, depending on its design.
Do this insteadCheck what the decorator does when the maximum attempt count is reached.
Control-Flow Practice
A retry decorator allows several attempts. On the first attempt, the wrapped function reaches a false assertion. The decorator catches the AssertionError and logs the failed attempt. Before looking at the answer, write the next four events in order.
Hints
- The decorator retries the entire wrapped function, not only the assertion line.
- The next execution starts at the beginning of the wrapped function.
- The decorator continues checking the attempt limit after each failure.
Practice Solution
Continue the trace after the first AssertionError is caught by a retry decorator.
- 11. Start a new full execution
The decorator invokes the wrapped function again from the beginning.
- 22. Observe the next outcome
The wrapped function either succeeds or raises another exception during this new execution.
- 33. Handle another failure if necessary
If the new execution fails, the decorator catches and logs that failure with its attempt number.
- 44. Compare with the maximum
The decorator retries again while attempts remain; after the limit, its design determines whether the last exception is raised or a default value is returned.
The control flow repeatedly cycles through full function execution, exception handling, logging, and an attempt-limit decision.
Trace Summary
- 1A false assert condition raises an
AssertionErrorimmediately and stops the current function. - 2A decorator sits between the caller and the wrapped function, so it can intercept exceptions from the wrapped operation.
- 3A retry decorator can log a failed attempt and re-execute the entire wrapped function up to a maximum number of attempts.
- 4The deepest traceback frame, listed last, identifies where the exception originated; the file name and line number pinpoint the failed assertion.
- 5Retries repeat the complete operation, so side effects can happen again on every attempt.
At a glance
- A false assertion changes normal execution into
AssertionErrorpropagation. - The decorator is the interception layer that can catch, log, retry, or propagate the exception.
- Automatic retry begins a new execution of the whole wrapped function.
- The last traceback frame is the best place to locate the original assertion failure.
- Always consider repeated side effects and the decorator's behavior after the retry limit.