Designing Idempotent Functions
A retry repeats the wrapped function as a whole. It does not restart only at the assertion that failed.
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…
Why Retries Need Careful Tracing
A retry decorator can make a failed operation run again automatically. That convenience creates an important execution question: what exactly happens between the first failure and the next attempt? The wrapped function does not resume at the failed assertion. The decorator catches the exception, records the failed attempt, and re-executes the entire wrapped function. This repeated execution is the central behavior to understand when designing functions for retry-based systems.
A retry repeats the wrapped function as a whole. It does not restart only at the assertion that failed.
The Assertion Failure
An assert statement checks a condition. When that condition is False, Python raises an AssertionError immediately. The current function stops at that point, and the exception begins propagating 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
Python raises an AssertionError and the decorator can handle it.
The false condition raises the exception immediately. Because the function is wrapped, the decorator is positioned to catch the exception before it reaches the caller, depending on the decorator's design.
Decorator Interception
A decorator sits between the caller and the wrapped function. The caller starts the decorated call, the decorator enters its execution process, and the decorator invokes the wrapped function. If the wrapped function raises an exception, control returns to the decorator's exception handler. The decorator can then log the failure, decide whether to retry, and either invoke the function again or allow the exception to propagate.
Retry Attempt Flow
A failed first attempt
Trace a decorated function whose first execution raises an AssertionError and whose retry executes the wrapped function again.
- 1Start
The caller invokes the decorated function. Control first enters the decorator.
- 2First execution
The decorator invokes the wrapped function. The function runs until its assert condition evaluates to False.
- 3Raise
Python raises an
AssertionErrorimmediately and stops the current function. - 4Intercept
The decorator catches the exception, logs the failure with the attempt number, and checks whether another attempt is allowed.
- 5Re-execute
If the retry policy allows another attempt, the decorator invokes the entire wrapped function again. It does not execute only the failed assertion.
- 6Finish
The next execution either succeeds, fails again and reaches another retry decision, or eventually reaches the maximum number of attempts.
The retry mechanism controls the transition between attempts, while the wrapped function starts over from its beginning on each re-execution.
A retry loop is controlled by the decorator, but the work inside each attempt belongs to the wrapped function. Any side effect performed before the failure, such as a database write, happens again when the entire function is re-executed.
Reading the Traceback
A traceback identifies where an exception occurred by showing the file name, line number, and function associated with the failure. When decorators are involved, the traceback can contain decorator frames because those frames are part of the call stack. To locate the actual source of the error, inspect the deepest frame, which is listed last. That frame shows where the exception originated.
- 1Find the exception type, such as
AssertionError. - 2Read the traceback frames in order.
- 3Treat the deepest frame, listed last, as the location where the exception originated.
- 4Use that frame's file name and line number to locate the failed assertion in the source.
- 5Do not mistake a decorator frame for the source of the assertion failure.
Design Implications
The retry behavior gives a practical design requirement for functions that may be executed more than once: examine every operation that occurs before a possible failure. Because the decorator re-executes the complete wrapped function, side effects are not automatically limited to one execution. A database write performed before an assertion can occur again on a later attempt. Designing an idempotent function therefore requires careful attention to what repeated execution does to those operations.
| Execution detail | What happens on failure | What happens on retry |
|---|---|---|
| Assertion | A false condition raises AssertionError | The assertion is encountered again when the function starts over |
| Wrapped function | The current execution stops | The entire function is re-executed |
| Decorator | Catches and logs the exception | Decides whether another attempt is allowed |
| Database write | May already have occurred before the failure | Can occur again during the next full execution |
The retry mechanism repeats the function, including operations performed before the failure.
Common mistake Assuming a retry resumes at the failed assertion
What it looks likeThe first attempt fails at an assertion, so the next attempt is imagined to begin on that same line.
Why it failsThe decorator re-executes the entire wrapped function rather than only the line that failed.
Do this insteadTrace the next attempt from the function's beginning and include every operation that runs before the assertion.
Common mistake Treating the decorator frame as the origin of the error
What it looks likeThe traceback includes decorator code, so the decorator is identified as the place where the assertion failed.
Why it failsDecorator frames are part of the call stack, but the deepest frame shows where the exception actually originated.
Do this insteadRead the last traceback frame and use its file name and line number to find the failed assertion.
Common mistake Assuming catching an exception means the operation succeeded
What it looks likeThe decorator logs the AssertionError, so the failure is treated as resolved.
Why it failsCatching lets the decorator decide whether to retry, propagate the last exception, or return a default value.
Do this insteadFollow the decorator's next decision after logging the failed attempt.
Common mistake Ignoring repeated side effects
What it looks likeA database write happens before the assertion, but the retry analysis considers only the assertion.
Why it failsThe entire function runs again, so the database write can happen again as well.
Do this insteadList the operations performed before the failure and account for each one on every attempt.
Trace It Yourself
A decorated function performs a database write, reaches an assert statement whose condition is False, and raises an AssertionError. The decorator logs the failed attempt and allows another attempt. Trace the order of events from the caller's invocation through the second execution. Identify which operation can happen twice and identify which traceback frame you would inspect first to locate the assertion.
Hints
- Begin with the decorator, not directly with the wrapped function.
- The first attempt stops when the assertion condition is False.
- The next attempt re-executes the whole wrapped function.
- The deepest traceback frame identifies the originating source location.
Practice trace answer
Determine the control flow for a database write followed by a failed assertion inside a retry-decorated function.
- 1First call
The caller enters the decorator, which invokes the wrapped function.
- 2First write
The wrapped function performs the database write before reaching the assertion.
- 3Failure
The false assertion condition raises
AssertionErrorand stops that execution of the function. - 4Retry decision
The decorator catches and logs the exception, then determines that another attempt is allowed.
- 5Second write
The decorator re-executes the entire wrapped function, so the database-write step is reached again.
- 6Traceback lookup
The deepest traceback frame points to the file name and line number where the assertion originated.
The database-write operation can occur on both executions, while the deepest traceback frame identifies the failed assertion's source location.
Key Takeaways
- 1A false assert condition raises
AssertionErrorimmediately and stops the current function. - 2A decorator sits between the caller and the wrapped function, allowing it to catch, log, and handle exceptions.
- 3A retry re-executes the entire wrapped function rather than only the failed line.
- 4The deepest traceback frame, listed last, identifies where the exception originated.
- 5Side effects such as database writes can happen again during a retry, so repeated execution must be considered when designing functions.
At a glance
- An
AssertionErrorbegins when an assert condition is False. - The decorator intercepts the exception after the wrapped function stops.
- The retry mechanism logs the failed attempt and may execute the whole function again.
- The deepest traceback frame shows the file name and line number where the assertion originated.
- Repeated execution can repeat side effects, including database writes.